From 528248b560db3aeba7a300bb34ccf6cf43384210 Mon Sep 17 00:00:00 2001 From: Jason Novinger Date: Mon, 10 Mar 2025 09:52:59 -0500 Subject: [PATCH 01/15] Fixes #18782: properly check if htmx_url is None If this is done incorrently, then the string formatting operation turns `htmx_url` into a string and the test in the template fails. --- netbox/extras/dashboard/widgets.py | 2 +- netbox/extras/tests/test_dashboard.py | 43 +++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 netbox/extras/tests/test_dashboard.py diff --git a/netbox/extras/dashboard/widgets.py b/netbox/extras/dashboard/widgets.py index eeed5414f..72c46edf4 100644 --- a/netbox/extras/dashboard/widgets.py +++ b/netbox/extras/dashboard/widgets.py @@ -257,7 +257,7 @@ class ObjectListWidget(DashboardWidget): parameters['per_page'] = page_size parameters['embedded'] = True - if parameters: + if parameters and htmx_url is not None: try: htmx_url = f'{htmx_url}?{urlencode(parameters, doseq=True)}' except ValueError: diff --git a/netbox/extras/tests/test_dashboard.py b/netbox/extras/tests/test_dashboard.py new file mode 100644 index 000000000..4705de1ab --- /dev/null +++ b/netbox/extras/tests/test_dashboard.py @@ -0,0 +1,43 @@ +from django.test import tag, TestCase + +from extras.dashboard.widgets import ObjectListWidget + + +class ObjectListWidgetTests(TestCase): + @tag('regression') + def test_widget_fails_gracefully(self): + """ + Example: + '2829fd9b-5dee-4c9a-81f2-5bd84c350a27': { + 'class': 'extras.ObjectListWidget', + 'color': 'indigo', + 'title': 'Object List', + 'config': { + 'model': 'extras.notification', + 'page_size': None, + 'url_params': None + } + } + """ + config = { + # 'class': 'extras.ObjectListWidget', # normally popped off, left for clarity + 'color': 'yellow', + 'title': 'this should fail', + 'config': { + 'model': 'extras.notification', + 'page_size': None, + 'url_params': None, + }, + } + + class Request: + class User: + def has_perm(self, *args, **kwargs): + return True + + user = User() + + mock_request = Request() + widget = ObjectListWidget(id='2829fd9b-5dee-4c9a-81f2-5bd84c350a27', **config) + rendered = widget.render(mock_request) + self.assertTrue('Unable to load content. Invalid view name:' in rendered) From 76c3c613a927965244ec8d123f557c9d1f7b3fd6 Mon Sep 17 00:00:00 2001 From: Jason Novinger Date: Mon, 10 Mar 2025 09:57:45 -0500 Subject: [PATCH 02/15] Adds validation for ObjectListWidget.ConfigForm.model field --- netbox/extras/dashboard/widgets.py | 33 +++++++++++++++++++++++++++ netbox/extras/tests/test_dashboard.py | 5 ++++ 2 files changed, 38 insertions(+) diff --git a/netbox/extras/dashboard/widgets.py b/netbox/extras/dashboard/widgets.py index 72c46edf4..207e9c5d7 100644 --- a/netbox/extras/dashboard/widgets.py +++ b/netbox/extras/dashboard/widgets.py @@ -9,6 +9,7 @@ import requests from django import forms from django.conf import settings from django.core.cache import cache +from django.db.models import Model from django.template.loader import render_to_string from django.urls import NoReverseMatch, resolve, reverse from django.utils.translation import gettext as _ @@ -42,6 +43,27 @@ def get_object_type_choices(): ] +def object_list_widget_supports_model(model: Model) -> bool: + """Test whether a model is supported by the ObjectListWidget + + In theory there could be more than one reason why a model isn't supported by the + ObjectListWidget, although we've only identified one so far--there's no resolve-able 'list' URL + for the model. Add more tests if more conditions arise. + """ + def can_resolve_model_list_view(model: Model) -> bool: + try: + reverse(get_viewname(model, action='list')) + return True + except Exception: + return False + + tests = [ + can_resolve_model_list_view, + ] + + return all(test(model) for test in tests) + + def get_bookmarks_object_type_choices(): return [ (object_type_identifier(ot), object_type_name(ot)) @@ -234,6 +256,17 @@ class ObjectListWidget(DashboardWidget): raise forms.ValidationError(_("Invalid format. URL parameters must be passed as a dictionary.")) return data + def clean_model(self): + if model_info := self.cleaned_data['model']: + app_label, model_name = model_info.split('.') + model = ObjectType.objects.get_by_natural_key(app_label, model_name).model_class() + if not object_list_widget_supports_model(model): + raise forms.ValidationError( + _(f"Invalid model selection: {self['model'].data} is not supported.") + ) + + return model_info + def render(self, request): app_label, model_name = self.config['model'].split('.') model = ObjectType.objects.get_by_natural_key(app_label, model_name).model_class() diff --git a/netbox/extras/tests/test_dashboard.py b/netbox/extras/tests/test_dashboard.py index 4705de1ab..19ce5a43d 100644 --- a/netbox/extras/tests/test_dashboard.py +++ b/netbox/extras/tests/test_dashboard.py @@ -4,6 +4,11 @@ from extras.dashboard.widgets import ObjectListWidget class ObjectListWidgetTests(TestCase): + def test_widget_config_form_validates_model(self): + model_info = 'extras.notification' + form = ObjectListWidget.ConfigForm({'model': model_info}) + self.assertFalse(form.is_valid()) + @tag('regression') def test_widget_fails_gracefully(self): """ From 994e7eb9f4583d5cbcb73dcbb0d6211c4ac05f0d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 13 Mar 2025 16:23:53 -0400 Subject: [PATCH 03/15] Fixes #18872: JournalEntry kind is a required field --- netbox/extras/forms/model_forms.py | 5 ++-- .../0123_journalentry_kind_default.py | 25 +++++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 netbox/extras/migrations/0123_journalentry_kind_default.py diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index be0b5856a..299fff81b 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -14,7 +14,7 @@ from netbox.events import get_event_type_choices from netbox.forms import NetBoxModelForm from tenancy.models import Tenant, TenantGroup from users.models import Group, User -from utilities.forms import add_blank_choice, get_field_value +from utilities.forms import get_field_value from utilities.forms.fields import ( CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, SlugField, @@ -687,8 +687,7 @@ class ImageAttachmentForm(forms.ModelForm): class JournalEntryForm(NetBoxModelForm): kind = forms.ChoiceField( label=_('Kind'), - choices=add_blank_choice(JournalEntryKindChoices), - required=False + choices=JournalEntryKindChoices ) comments = CommentField() diff --git a/netbox/extras/migrations/0123_journalentry_kind_default.py b/netbox/extras/migrations/0123_journalentry_kind_default.py new file mode 100644 index 000000000..b32960469 --- /dev/null +++ b/netbox/extras/migrations/0123_journalentry_kind_default.py @@ -0,0 +1,25 @@ +from django.db import migrations + +from extras.choices import JournalEntryKindChoices + + +def set_kind_default(apps, schema_editor): + """ + Set kind to "info" on any entries with no kind assigned. + """ + JournalEntry = apps.get_model('extras', 'JournalEntry') + JournalEntry.objects.filter(kind='').update(kind=JournalEntryKindChoices.KIND_INFO) + + +class Migration(migrations.Migration): + + dependencies = [ + ('extras', '0122_charfield_null_choices'), + ] + + operations = [ + migrations.RunPython( + code=set_kind_default, + reverse_code=migrations.RunPython.noop + ), + ] From c5801f988127cdb764d765b1452e1b4d46f7c834 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 17 Mar 2025 14:16:55 -0400 Subject: [PATCH 04/15] Fixes #18928: Fix support for cascading deletions when cleaning up expired changelog records --- netbox/extras/management/commands/housekeeping.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/netbox/extras/management/commands/housekeeping.py b/netbox/extras/management/commands/housekeeping.py index ade486fc0..aafba8c0d 100644 --- a/netbox/extras/management/commands/housekeeping.py +++ b/netbox/extras/management/commands/housekeeping.py @@ -5,7 +5,6 @@ import requests from django.conf import settings from django.core.cache import cache from django.core.management.base import BaseCommand -from django.db import DEFAULT_DB_ALIAS from django.utils import timezone from packaging import version @@ -53,7 +52,7 @@ class Command(BaseCommand): ending="" ) self.stdout.flush() - ObjectChange.objects.filter(time__lt=cutoff)._raw_delete(using=DEFAULT_DB_ALIAS) + ObjectChange.objects.filter(time__lt=cutoff).delete() if options['verbosity']: self.stdout.write("Done.", self.style.SUCCESS) elif options['verbosity']: From dce694afa9471d85e7888e83271a921595c6b3e8 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 18 Mar 2025 05:02:07 +0000 Subject: [PATCH 05/15] Update source translation strings --- netbox/translations/en/LC_MESSAGES/django.po | 58 ++++++++++---------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/netbox/translations/en/LC_MESSAGES/django.po b/netbox/translations/en/LC_MESSAGES/django.po index 5fd90cb5d..e37c242ea 100644 --- a/netbox/translations/en/LC_MESSAGES/django.po +++ b/netbox/translations/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-03-14 05:01+0000\n" +"POT-Creation-Date: 2025-03-18 05:01+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -84,9 +84,9 @@ msgstr "" #: netbox/circuits/choices.py:21 netbox/dcim/choices.py:20 #: netbox/dcim/choices.py:102 netbox/dcim/choices.py:185 -#: netbox/dcim/choices.py:237 netbox/dcim/choices.py:1534 -#: netbox/dcim/choices.py:1592 netbox/dcim/choices.py:1642 -#: netbox/dcim/choices.py:1664 netbox/virtualization/choices.py:20 +#: netbox/dcim/choices.py:237 netbox/dcim/choices.py:1540 +#: netbox/dcim/choices.py:1598 netbox/dcim/choices.py:1648 +#: netbox/dcim/choices.py:1670 netbox/virtualization/choices.py:20 #: netbox/virtualization/choices.py:46 netbox/vpn/choices.py:18 msgid "Planned" msgstr "" @@ -98,8 +98,8 @@ msgstr "" #: netbox/circuits/choices.py:23 netbox/core/tables/tasks.py:22 #: netbox/dcim/choices.py:22 netbox/dcim/choices.py:103 #: netbox/dcim/choices.py:184 netbox/dcim/choices.py:236 -#: netbox/dcim/choices.py:1591 netbox/dcim/choices.py:1641 -#: netbox/dcim/choices.py:1663 netbox/extras/tables/tables.py:495 +#: netbox/dcim/choices.py:1597 netbox/dcim/choices.py:1647 +#: netbox/dcim/choices.py:1669 netbox/extras/tables/tables.py:495 #: netbox/ipam/choices.py:31 netbox/ipam/choices.py:49 #: netbox/ipam/choices.py:69 netbox/ipam/choices.py:154 #: netbox/templates/extras/configcontext.html:25 @@ -110,8 +110,8 @@ msgid "Active" msgstr "" #: netbox/circuits/choices.py:24 netbox/dcim/choices.py:183 -#: netbox/dcim/choices.py:235 netbox/dcim/choices.py:1590 -#: netbox/dcim/choices.py:1643 netbox/dcim/choices.py:1662 +#: netbox/dcim/choices.py:235 netbox/dcim/choices.py:1596 +#: netbox/dcim/choices.py:1649 netbox/dcim/choices.py:1668 #: netbox/virtualization/choices.py:24 netbox/virtualization/choices.py:44 msgid "Offline" msgstr "" @@ -124,7 +124,7 @@ msgstr "" msgid "Decommissioned" msgstr "" -#: netbox/circuits/choices.py:90 netbox/dcim/choices.py:1603 +#: netbox/circuits/choices.py:90 netbox/dcim/choices.py:1609 #: netbox/templates/dcim/interface.html:135 #: netbox/templates/virtualization/vminterface.html:77 #: netbox/tenancy/choices.py:17 @@ -2014,7 +2014,7 @@ msgstr "" #: netbox/core/choices.py:22 netbox/core/choices.py:59 #: netbox/core/constants.py:20 netbox/core/tables/tasks.py:34 #: netbox/dcim/choices.py:187 netbox/dcim/choices.py:239 -#: netbox/dcim/choices.py:1593 netbox/dcim/choices.py:1666 +#: netbox/dcim/choices.py:1599 netbox/dcim/choices.py:1672 #: netbox/virtualization/choices.py:48 msgid "Failed" msgstr "" @@ -2362,7 +2362,7 @@ msgstr "" msgid "Rack Elevations" msgstr "" -#: netbox/core/forms/model_forms.py:157 netbox/dcim/choices.py:1522 +#: netbox/core/forms/model_forms.py:157 netbox/dcim/choices.py:1528 #: netbox/dcim/forms/bulk_edit.py:987 netbox/dcim/forms/bulk_edit.py:1375 #: netbox/dcim/forms/bulk_edit.py:1393 netbox/dcim/tables/racks.py:157 #: netbox/netbox/navigation/menu.py:312 netbox/netbox/navigation/menu.py:316 @@ -2946,8 +2946,8 @@ msgid "Staging" msgstr "" #: netbox/dcim/choices.py:23 netbox/dcim/choices.py:189 -#: netbox/dcim/choices.py:240 netbox/dcim/choices.py:1535 -#: netbox/dcim/choices.py:1667 netbox/virtualization/choices.py:23 +#: netbox/dcim/choices.py:240 netbox/dcim/choices.py:1541 +#: netbox/dcim/choices.py:1673 netbox/virtualization/choices.py:23 #: netbox/virtualization/choices.py:49 msgid "Decommissioning" msgstr "" @@ -3011,7 +3011,7 @@ msgstr "" msgid "Millimeters" msgstr "" -#: netbox/dcim/choices.py:115 netbox/dcim/choices.py:1557 +#: netbox/dcim/choices.py:115 netbox/dcim/choices.py:1563 msgid "Inches" msgstr "" @@ -3080,7 +3080,7 @@ msgid "Rear" msgstr "" #: netbox/dcim/choices.py:186 netbox/dcim/choices.py:238 -#: netbox/dcim/choices.py:1665 netbox/virtualization/choices.py:47 +#: netbox/dcim/choices.py:1671 netbox/virtualization/choices.py:47 msgid "Staged" msgstr "" @@ -3143,7 +3143,7 @@ msgstr "" #: netbox/dcim/choices.py:581 netbox/dcim/choices.py:824 #: netbox/dcim/choices.py:1221 netbox/dcim/choices.py:1223 -#: netbox/dcim/choices.py:1451 netbox/dcim/choices.py:1453 +#: netbox/dcim/choices.py:1457 netbox/dcim/choices.py:1459 #: netbox/netbox/navigation/menu.py:208 msgid "Other" msgstr "" @@ -3269,57 +3269,57 @@ msgstr "" msgid "Passive 48V (4-pair)" msgstr "" -#: netbox/dcim/choices.py:1382 netbox/dcim/choices.py:1492 +#: netbox/dcim/choices.py:1385 netbox/dcim/choices.py:1498 msgid "Copper" msgstr "" -#: netbox/dcim/choices.py:1405 +#: netbox/dcim/choices.py:1408 msgid "Fiber Optic" msgstr "" -#: netbox/dcim/choices.py:1438 netbox/dcim/choices.py:1521 +#: netbox/dcim/choices.py:1444 netbox/dcim/choices.py:1527 msgid "USB" msgstr "" -#: netbox/dcim/choices.py:1508 +#: netbox/dcim/choices.py:1514 msgid "Fiber" msgstr "" -#: netbox/dcim/choices.py:1533 netbox/dcim/forms/filtersets.py:1228 +#: netbox/dcim/choices.py:1539 netbox/dcim/forms/filtersets.py:1228 msgid "Connected" msgstr "" -#: netbox/dcim/choices.py:1552 netbox/netbox/choices.py:175 +#: netbox/dcim/choices.py:1558 netbox/netbox/choices.py:175 msgid "Kilometers" msgstr "" -#: netbox/dcim/choices.py:1553 netbox/netbox/choices.py:176 +#: netbox/dcim/choices.py:1559 netbox/netbox/choices.py:176 #: netbox/templates/dcim/cable_trace.html:65 msgid "Meters" msgstr "" -#: netbox/dcim/choices.py:1554 +#: netbox/dcim/choices.py:1560 msgid "Centimeters" msgstr "" -#: netbox/dcim/choices.py:1555 netbox/netbox/choices.py:177 +#: netbox/dcim/choices.py:1561 netbox/netbox/choices.py:177 msgid "Miles" msgstr "" -#: netbox/dcim/choices.py:1556 netbox/netbox/choices.py:178 +#: netbox/dcim/choices.py:1562 netbox/netbox/choices.py:178 #: netbox/templates/dcim/cable_trace.html:66 msgid "Feet" msgstr "" -#: netbox/dcim/choices.py:1604 +#: netbox/dcim/choices.py:1610 msgid "Redundant" msgstr "" -#: netbox/dcim/choices.py:1625 +#: netbox/dcim/choices.py:1631 msgid "Single phase" msgstr "" -#: netbox/dcim/choices.py:1626 +#: netbox/dcim/choices.py:1632 msgid "Three-phase" msgstr "" From d7709a2a55f8c5290f3f83831675ce0256733869 Mon Sep 17 00:00:00 2001 From: Jason Novinger Date: Mon, 17 Mar 2025 17:39:43 -0500 Subject: [PATCH 06/15] Fixes #18926: Uses correct icon for base GitHub auth --- netbox/netbox/authentication/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/authentication/__init__.py b/netbox/netbox/authentication/__init__.py index 83f699e42..25f9b902c 100644 --- a/netbox/netbox/authentication/__init__.py +++ b/netbox/netbox/authentication/__init__.py @@ -28,7 +28,7 @@ AUTH_BACKEND_ATTRS = { 'bitbucket-oauth2': ('BitBucket', 'bitbucket'), 'digitalocean': ('DigitalOcean', 'digital-ocean'), 'docker': ('Docker', 'docker'), - 'github': ('GitHub', 'docker'), + 'github': ('GitHub', 'github'), 'github-app': ('GitHub', 'github'), 'github-org': ('GitHub', 'github'), 'github-team': ('GitHub', 'github'), From 43840e6a72416a5e0c963b230f9471308a29e3e1 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 19 Mar 2025 05:02:13 +0000 Subject: [PATCH 07/15] Update source translation strings --- netbox/translations/en/LC_MESSAGES/django.po | 44 +++++++++++--------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/netbox/translations/en/LC_MESSAGES/django.po b/netbox/translations/en/LC_MESSAGES/django.po index e37c242ea..c8c53c062 100644 --- a/netbox/translations/en/LC_MESSAGES/django.po +++ b/netbox/translations/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-03-18 05:01+0000\n" +"POT-Creation-Date: 2025-03-19 05:01+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -7777,84 +7777,88 @@ msgstr "" msgid "Unregistered widget class: {name}" msgstr "" -#: netbox/extras/dashboard/widgets.py:125 +#: netbox/extras/dashboard/widgets.py:147 #, python-brace-format msgid "{class_name} must define a render() method." msgstr "" -#: netbox/extras/dashboard/widgets.py:144 +#: netbox/extras/dashboard/widgets.py:166 msgid "Note" msgstr "" -#: netbox/extras/dashboard/widgets.py:145 +#: netbox/extras/dashboard/widgets.py:167 msgid "Display some arbitrary custom content. Markdown is supported." msgstr "" -#: netbox/extras/dashboard/widgets.py:158 +#: netbox/extras/dashboard/widgets.py:180 msgid "Object Counts" msgstr "" -#: netbox/extras/dashboard/widgets.py:159 +#: netbox/extras/dashboard/widgets.py:181 msgid "" "Display a set of NetBox models and the number of objects created for each " "type." msgstr "" -#: netbox/extras/dashboard/widgets.py:169 +#: netbox/extras/dashboard/widgets.py:191 msgid "Filters to apply when counting the number of objects" msgstr "" -#: netbox/extras/dashboard/widgets.py:177 +#: netbox/extras/dashboard/widgets.py:199 msgid "Invalid format. Object filters must be passed as a dictionary." msgstr "" -#: netbox/extras/dashboard/widgets.py:208 +#: netbox/extras/dashboard/widgets.py:230 msgid "Object List" msgstr "" -#: netbox/extras/dashboard/widgets.py:209 +#: netbox/extras/dashboard/widgets.py:231 msgid "Display an arbitrary list of objects." msgstr "" -#: netbox/extras/dashboard/widgets.py:222 +#: netbox/extras/dashboard/widgets.py:244 msgid "The default number of objects to display" msgstr "" -#: netbox/extras/dashboard/widgets.py:234 +#: netbox/extras/dashboard/widgets.py:256 msgid "Invalid format. URL parameters must be passed as a dictionary." msgstr "" -#: netbox/extras/dashboard/widgets.py:274 +#: netbox/extras/dashboard/widgets.py:265 +msgid "Invalid model selection: {self['model'].data} is not supported." +msgstr "" + +#: netbox/extras/dashboard/widgets.py:307 msgid "RSS Feed" msgstr "" -#: netbox/extras/dashboard/widgets.py:280 +#: netbox/extras/dashboard/widgets.py:313 msgid "Embed an RSS feed from an external website." msgstr "" -#: netbox/extras/dashboard/widgets.py:287 +#: netbox/extras/dashboard/widgets.py:320 msgid "Feed URL" msgstr "" -#: netbox/extras/dashboard/widgets.py:291 +#: netbox/extras/dashboard/widgets.py:324 msgid "Requires external connection" msgstr "" -#: netbox/extras/dashboard/widgets.py:297 +#: netbox/extras/dashboard/widgets.py:330 msgid "The maximum number of objects to display" msgstr "" -#: netbox/extras/dashboard/widgets.py:302 +#: netbox/extras/dashboard/widgets.py:335 msgid "How long to stored the cached content (in seconds)" msgstr "" -#: netbox/extras/dashboard/widgets.py:359 netbox/templates/account/base.html:10 +#: netbox/extras/dashboard/widgets.py:392 netbox/templates/account/base.html:10 #: netbox/templates/account/bookmarks.html:7 #: netbox/templates/inc/user_menu.html:43 msgid "Bookmarks" msgstr "" -#: netbox/extras/dashboard/widgets.py:363 +#: netbox/extras/dashboard/widgets.py:396 msgid "Show your personal bookmarks" msgstr "" From bf1a9a6e2d3c4d3f28ee5c5dab5b0e12c8685d80 Mon Sep 17 00:00:00 2001 From: Renato Almeida de Oliveira Date: Wed, 19 Mar 2025 10:56:42 -0300 Subject: [PATCH 08/15] Fixes: #18833 Inventory Item Bulk Import - 'InventoryItemImportForm' has no field named 'component_id'. (#18874) * Refactor InventoryItemImportForm clean method * Add super().clean(); renamed content_type; simplified component creation * Fix missing component_name issue * Update netbox/dcim/forms/bulk_import.py --------- Co-authored-by: Jeremy Stretch --- netbox/dcim/forms/bulk_import.py | 54 +++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index 92f7220da..cb36e94bf 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -1161,27 +1161,45 @@ class InventoryItemImportForm(NetBoxModelImportForm): else: self.fields['parent'].queryset = InventoryItem.objects.none() - def clean_component_name(self): - content_type = self.cleaned_data.get('component_type') - component_name = self.cleaned_data.get('component_name') + def clean(self): + super().clean() + cleaned_data = self.cleaned_data + component_type = cleaned_data.get('component_type') + component_name = cleaned_data.get('component_name') device = self.cleaned_data.get("device") - if not device and hasattr(self, 'instance') and hasattr(self.instance, 'device'): - device = self.instance.device - - if not all([device, content_type, component_name]): - return None - - model = content_type.model_class() - try: - component = model.objects.get(device=device, name=component_name) - self.instance.component = component - except ObjectDoesNotExist: - raise forms.ValidationError( - _("Component not found: {device} - {component_name}").format( - device=device, component_name=component_name + if component_type: + if device is None: + cleaned_data.pop('component_type', None) + if component_name is None: + cleaned_data.pop('component_type', None) + raise forms.ValidationError( + _("Component name must be specified when component type is specified") ) - ) + if all([device, component_name]): + try: + model = component_type.model_class() + self.instance.component = model.objects.get(device=device, name=component_name) + except ObjectDoesNotExist: + cleaned_data.pop('component_type', None) + cleaned_data.pop('component_name', None) + raise forms.ValidationError( + _("Component not found: {device} - {component_name}").format( + device=device, component_name=component_name + ) + ) + else: + cleaned_data.pop('component_type', None) + if not component_name: + raise forms.ValidationError( + _("Component name must be specified when component type is specified") + ) + else: + if component_name: + raise forms.ValidationError( + _("Component type must be specified when component name is specified") + ) + return cleaned_data # From 9da4cf31abf452891fb8189c6d040d59a87555d1 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 20 Mar 2025 05:02:02 +0000 Subject: [PATCH 09/15] Update source translation strings --- netbox/translations/en/LC_MESSAGES/django.po | 126 ++++++++++--------- 1 file changed, 67 insertions(+), 59 deletions(-) diff --git a/netbox/translations/en/LC_MESSAGES/django.po b/netbox/translations/en/LC_MESSAGES/django.po index c8c53c062..c308d5fc9 100644 --- a/netbox/translations/en/LC_MESSAGES/django.po +++ b/netbox/translations/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-03-19 05:01+0000\n" +"POT-Creation-Date: 2025-03-20 05:01+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -210,8 +210,8 @@ msgstr "" #: netbox/dcim/forms/bulk_edit.py:333 netbox/dcim/forms/bulk_edit.py:686 #: netbox/dcim/forms/bulk_edit.py:891 netbox/dcim/forms/bulk_import.py:133 #: netbox/dcim/forms/bulk_import.py:232 netbox/dcim/forms/bulk_import.py:333 -#: netbox/dcim/forms/bulk_import.py:567 netbox/dcim/forms/bulk_import.py:1430 -#: netbox/dcim/forms/bulk_import.py:1458 netbox/dcim/forms/filtersets.py:88 +#: netbox/dcim/forms/bulk_import.py:567 netbox/dcim/forms/bulk_import.py:1448 +#: netbox/dcim/forms/bulk_import.py:1476 netbox/dcim/forms/filtersets.py:88 #: netbox/dcim/forms/filtersets.py:226 netbox/dcim/forms/filtersets.py:343 #: netbox/dcim/forms/filtersets.py:440 netbox/dcim/forms/filtersets.py:754 #: netbox/dcim/forms/filtersets.py:998 netbox/dcim/forms/filtersets.py:1022 @@ -679,8 +679,8 @@ msgstr "" #: netbox/dcim/forms/bulk_import.py:735 netbox/dcim/forms/bulk_import.py:761 #: netbox/dcim/forms/bulk_import.py:787 netbox/dcim/forms/bulk_import.py:807 #: netbox/dcim/forms/bulk_import.py:893 netbox/dcim/forms/bulk_import.py:987 -#: netbox/dcim/forms/bulk_import.py:1029 netbox/dcim/forms/bulk_import.py:1332 -#: netbox/dcim/forms/bulk_import.py:1495 netbox/dcim/forms/filtersets.py:956 +#: netbox/dcim/forms/bulk_import.py:1029 netbox/dcim/forms/bulk_import.py:1350 +#: netbox/dcim/forms/bulk_import.py:1513 netbox/dcim/forms/filtersets.py:956 #: netbox/dcim/forms/filtersets.py:1055 netbox/dcim/forms/filtersets.py:1176 #: netbox/dcim/forms/filtersets.py:1248 netbox/dcim/forms/filtersets.py:1273 #: netbox/dcim/forms/filtersets.py:1297 netbox/dcim/forms/filtersets.py:1317 @@ -750,8 +750,8 @@ msgstr "" #: netbox/dcim/forms/bulk_edit.py:1765 netbox/dcim/forms/bulk_import.py:90 #: netbox/dcim/forms/bulk_import.py:149 netbox/dcim/forms/bulk_import.py:250 #: netbox/dcim/forms/bulk_import.py:532 netbox/dcim/forms/bulk_import.py:686 -#: netbox/dcim/forms/bulk_import.py:1137 netbox/dcim/forms/bulk_import.py:1326 -#: netbox/dcim/forms/bulk_import.py:1490 netbox/dcim/forms/bulk_import.py:1554 +#: netbox/dcim/forms/bulk_import.py:1137 netbox/dcim/forms/bulk_import.py:1344 +#: netbox/dcim/forms/bulk_import.py:1508 netbox/dcim/forms/bulk_import.py:1572 #: netbox/dcim/forms/filtersets.py:179 netbox/dcim/forms/filtersets.py:238 #: netbox/dcim/forms/filtersets.py:360 netbox/dcim/forms/filtersets.py:800 #: netbox/dcim/forms/filtersets.py:925 netbox/dcim/forms/filtersets.py:959 @@ -824,8 +824,8 @@ msgstr "" #: netbox/dcim/forms/bulk_edit.py:812 netbox/dcim/forms/bulk_edit.py:1770 #: netbox/dcim/forms/bulk_import.py:109 netbox/dcim/forms/bulk_import.py:154 #: netbox/dcim/forms/bulk_import.py:243 netbox/dcim/forms/bulk_import.py:358 -#: netbox/dcim/forms/bulk_import.py:506 netbox/dcim/forms/bulk_import.py:1338 -#: netbox/dcim/forms/bulk_import.py:1547 netbox/dcim/forms/filtersets.py:174 +#: netbox/dcim/forms/bulk_import.py:506 netbox/dcim/forms/bulk_import.py:1356 +#: netbox/dcim/forms/bulk_import.py:1565 netbox/dcim/forms/filtersets.py:174 #: netbox/dcim/forms/filtersets.py:206 netbox/dcim/forms/filtersets.py:324 #: netbox/dcim/forms/filtersets.py:400 netbox/dcim/forms/filtersets.py:421 #: netbox/dcim/forms/filtersets.py:723 netbox/dcim/forms/filtersets.py:917 @@ -991,7 +991,7 @@ msgstr "" #: netbox/circuits/forms/bulk_edit.py:215 #: netbox/circuits/forms/model_forms.py:170 -#: netbox/dcim/forms/bulk_import.py:1299 netbox/dcim/forms/bulk_import.py:1317 +#: netbox/dcim/forms/bulk_import.py:1317 netbox/dcim/forms/bulk_import.py:1335 msgid "Termination type" msgstr "" @@ -1127,7 +1127,7 @@ msgstr "" #: netbox/circuits/forms/bulk_import.py:229 netbox/dcim/forms/bulk_import.py:92 #: netbox/dcim/forms/bulk_import.py:151 netbox/dcim/forms/bulk_import.py:252 #: netbox/dcim/forms/bulk_import.py:534 netbox/dcim/forms/bulk_import.py:688 -#: netbox/dcim/forms/bulk_import.py:1139 netbox/dcim/forms/bulk_import.py:1492 +#: netbox/dcim/forms/bulk_import.py:1139 netbox/dcim/forms/bulk_import.py:1510 #: netbox/ipam/forms/bulk_import.py:197 netbox/ipam/forms/bulk_import.py:265 #: netbox/ipam/forms/bulk_import.py:301 netbox/ipam/forms/bulk_import.py:482 #: netbox/ipam/forms/bulk_import.py:495 @@ -1142,8 +1142,8 @@ msgstr "" #: netbox/circuits/forms/bulk_import.py:236 #: netbox/dcim/forms/bulk_import.py:113 netbox/dcim/forms/bulk_import.py:158 #: netbox/dcim/forms/bulk_import.py:362 netbox/dcim/forms/bulk_import.py:510 -#: netbox/dcim/forms/bulk_import.py:1342 netbox/dcim/forms/bulk_import.py:1487 -#: netbox/dcim/forms/bulk_import.py:1551 netbox/ipam/forms/bulk_import.py:45 +#: netbox/dcim/forms/bulk_import.py:1360 netbox/dcim/forms/bulk_import.py:1505 +#: netbox/dcim/forms/bulk_import.py:1569 netbox/ipam/forms/bulk_import.py:45 #: netbox/ipam/forms/bulk_import.py:74 netbox/ipam/forms/bulk_import.py:102 #: netbox/ipam/forms/bulk_import.py:122 netbox/ipam/forms/bulk_import.py:142 #: netbox/ipam/forms/bulk_import.py:171 netbox/ipam/forms/bulk_import.py:260 @@ -1187,7 +1187,7 @@ msgstr "" #: netbox/circuits/forms/bulk_import.py:259 #: netbox/circuits/forms/model_forms.py:368 #: netbox/circuits/tables/virtual_circuits.py:112 -#: netbox/dcim/forms/bulk_import.py:1219 netbox/dcim/forms/model_forms.py:1164 +#: netbox/dcim/forms/bulk_import.py:1237 netbox/dcim/forms/model_forms.py:1164 #: netbox/dcim/forms/model_forms.py:1433 netbox/dcim/forms/model_forms.py:1600 #: netbox/dcim/forms/model_forms.py:1635 netbox/dcim/forms/model_forms.py:1765 #: netbox/dcim/tables/connections.py:65 netbox/dcim/tables/devices.py:1140 @@ -1222,8 +1222,8 @@ msgstr "" #: netbox/dcim/forms/bulk_edit.py:450 netbox/dcim/forms/bulk_edit.py:691 #: netbox/dcim/forms/bulk_edit.py:746 netbox/dcim/forms/bulk_edit.py:900 #: netbox/dcim/forms/bulk_import.py:237 netbox/dcim/forms/bulk_import.py:339 -#: netbox/dcim/forms/bulk_import.py:573 netbox/dcim/forms/bulk_import.py:1436 -#: netbox/dcim/forms/bulk_import.py:1470 netbox/dcim/forms/filtersets.py:96 +#: netbox/dcim/forms/bulk_import.py:573 netbox/dcim/forms/bulk_import.py:1454 +#: netbox/dcim/forms/bulk_import.py:1488 netbox/dcim/forms/filtersets.py:96 #: netbox/dcim/forms/filtersets.py:323 netbox/dcim/forms/filtersets.py:357 #: netbox/dcim/forms/filtersets.py:397 netbox/dcim/forms/filtersets.py:448 #: netbox/dcim/forms/filtersets.py:720 netbox/dcim/forms/filtersets.py:763 @@ -1917,8 +1917,8 @@ msgstr "" #: netbox/dcim/forms/bulk_import.py:802 netbox/dcim/forms/bulk_import.py:858 #: netbox/dcim/forms/bulk_import.py:976 netbox/dcim/forms/bulk_import.py:1024 #: netbox/dcim/forms/bulk_import.py:1041 netbox/dcim/forms/bulk_import.py:1053 -#: netbox/dcim/forms/bulk_import.py:1101 netbox/dcim/forms/bulk_import.py:1205 -#: netbox/dcim/forms/bulk_import.py:1541 netbox/dcim/forms/connections.py:24 +#: netbox/dcim/forms/bulk_import.py:1101 netbox/dcim/forms/bulk_import.py:1223 +#: netbox/dcim/forms/bulk_import.py:1559 netbox/dcim/forms/connections.py:24 #: netbox/dcim/forms/filtersets.py:132 netbox/dcim/forms/filtersets.py:922 #: netbox/dcim/forms/filtersets.py:1052 netbox/dcim/forms/filtersets.py:1243 #: netbox/dcim/forms/filtersets.py:1268 netbox/dcim/forms/filtersets.py:1292 @@ -4088,8 +4088,8 @@ msgstr "" #: netbox/dcim/forms/bulk_edit.py:449 netbox/dcim/forms/bulk_edit.py:928 #: netbox/dcim/forms/bulk_import.py:346 netbox/dcim/forms/bulk_import.py:349 -#: netbox/dcim/forms/bulk_import.py:580 netbox/dcim/forms/bulk_import.py:1477 -#: netbox/dcim/forms/bulk_import.py:1481 netbox/dcim/forms/filtersets.py:105 +#: netbox/dcim/forms/bulk_import.py:580 netbox/dcim/forms/bulk_import.py:1495 +#: netbox/dcim/forms/bulk_import.py:1499 netbox/dcim/forms/filtersets.py:105 #: netbox/dcim/forms/filtersets.py:325 netbox/dcim/forms/filtersets.py:406 #: netbox/dcim/forms/filtersets.py:420 netbox/dcim/forms/filtersets.py:458 #: netbox/dcim/forms/filtersets.py:773 netbox/dcim/forms/filtersets.py:1036 @@ -4266,8 +4266,8 @@ msgstr "" msgid "Length" msgstr "" -#: netbox/dcim/forms/bulk_edit.py:831 netbox/dcim/forms/bulk_import.py:1345 -#: netbox/dcim/forms/bulk_import.py:1348 netbox/dcim/forms/filtersets.py:1073 +#: netbox/dcim/forms/bulk_edit.py:831 netbox/dcim/forms/bulk_import.py:1363 +#: netbox/dcim/forms/bulk_import.py:1366 netbox/dcim/forms/filtersets.py:1073 msgid "Length unit" msgstr "" @@ -4276,17 +4276,17 @@ msgstr "" msgid "Domain" msgstr "" -#: netbox/dcim/forms/bulk_edit.py:923 netbox/dcim/forms/bulk_import.py:1464 +#: netbox/dcim/forms/bulk_edit.py:923 netbox/dcim/forms/bulk_import.py:1482 #: netbox/dcim/forms/filtersets.py:1159 netbox/dcim/forms/model_forms.py:761 msgid "Power panel" msgstr "" -#: netbox/dcim/forms/bulk_edit.py:945 netbox/dcim/forms/bulk_import.py:1500 +#: netbox/dcim/forms/bulk_edit.py:945 netbox/dcim/forms/bulk_import.py:1518 #: netbox/dcim/forms/filtersets.py:1181 netbox/templates/dcim/powerfeed.html:83 msgid "Supply" msgstr "" -#: netbox/dcim/forms/bulk_edit.py:951 netbox/dcim/forms/bulk_import.py:1505 +#: netbox/dcim/forms/bulk_edit.py:951 netbox/dcim/forms/bulk_import.py:1523 #: netbox/dcim/forms/filtersets.py:1186 netbox/templates/dcim/powerfeed.html:95 msgid "Phase" msgstr "" @@ -4522,7 +4522,7 @@ msgid "available options" msgstr "" #: netbox/dcim/forms/bulk_import.py:136 netbox/dcim/forms/bulk_import.py:570 -#: netbox/dcim/forms/bulk_import.py:1461 netbox/ipam/forms/bulk_import.py:463 +#: netbox/dcim/forms/bulk_import.py:1479 netbox/ipam/forms/bulk_import.py:463 #: netbox/virtualization/forms/bulk_import.py:64 #: netbox/virtualization/forms/bulk_import.py:95 msgid "Assigned site" @@ -4585,7 +4585,7 @@ msgstr "" msgid "Parent site" msgstr "" -#: netbox/dcim/forms/bulk_import.py:343 netbox/dcim/forms/bulk_import.py:1474 +#: netbox/dcim/forms/bulk_import.py:343 netbox/dcim/forms/bulk_import.py:1492 msgid "Rack's location (if any)" msgstr "" @@ -4628,7 +4628,7 @@ msgstr "" msgid "Limit platform assignments to this manufacturer" msgstr "" -#: netbox/dcim/forms/bulk_import.py:503 netbox/dcim/forms/bulk_import.py:1544 +#: netbox/dcim/forms/bulk_import.py:503 netbox/dcim/forms/bulk_import.py:1562 #: netbox/tenancy/forms/bulk_import.py:106 msgid "Assigned role" msgstr "" @@ -4818,7 +4818,7 @@ msgid "Corresponding rear port" msgstr "" #: netbox/dcim/forms/bulk_import.py:989 netbox/dcim/forms/bulk_import.py:1030 -#: netbox/dcim/forms/bulk_import.py:1335 +#: netbox/dcim/forms/bulk_import.py:1353 msgid "Physical medium classification" msgstr "" @@ -4854,16 +4854,24 @@ msgstr "" msgid "Component Name" msgstr "" -#: netbox/dcim/forms/bulk_import.py:1181 +#: netbox/dcim/forms/bulk_import.py:1177 netbox/dcim/forms/bulk_import.py:1195 +msgid "Component name must be specified when component type is specified" +msgstr "" + +#: netbox/dcim/forms/bulk_import.py:1187 #, python-brace-format msgid "Component not found: {device} - {component_name}" msgstr "" -#: netbox/dcim/forms/bulk_import.py:1209 netbox/ipam/forms/bulk_import.py:314 +#: netbox/dcim/forms/bulk_import.py:1200 +msgid "Component type must be specified when component name is specified" +msgstr "" + +#: netbox/dcim/forms/bulk_import.py:1227 netbox/ipam/forms/bulk_import.py:314 msgid "Parent device of assigned interface (if any)" msgstr "" -#: netbox/dcim/forms/bulk_import.py:1212 netbox/ipam/forms/bulk_import.py:317 +#: netbox/dcim/forms/bulk_import.py:1230 netbox/ipam/forms/bulk_import.py:317 #: netbox/ipam/forms/bulk_import.py:554 netbox/ipam/forms/model_forms.py:768 #: netbox/virtualization/filtersets.py:254 #: netbox/virtualization/filtersets.py:305 @@ -4878,124 +4886,124 @@ msgstr "" msgid "Virtual machine" msgstr "" -#: netbox/dcim/forms/bulk_import.py:1216 netbox/ipam/forms/bulk_import.py:321 +#: netbox/dcim/forms/bulk_import.py:1234 netbox/ipam/forms/bulk_import.py:321 msgid "Parent VM of assigned interface (if any)" msgstr "" -#: netbox/dcim/forms/bulk_import.py:1223 netbox/ipam/filtersets.py:1022 +#: netbox/dcim/forms/bulk_import.py:1241 netbox/ipam/filtersets.py:1022 #: netbox/ipam/forms/bulk_import.py:328 msgid "Assigned interface" msgstr "" -#: netbox/dcim/forms/bulk_import.py:1226 netbox/ipam/forms/bulk_import.py:331 +#: netbox/dcim/forms/bulk_import.py:1244 netbox/ipam/forms/bulk_import.py:331 msgid "Is primary" msgstr "" -#: netbox/dcim/forms/bulk_import.py:1227 +#: netbox/dcim/forms/bulk_import.py:1245 msgid "Make this the primary MAC address for the assigned interface" msgstr "" -#: netbox/dcim/forms/bulk_import.py:1264 +#: netbox/dcim/forms/bulk_import.py:1282 msgid "Must specify the parent device or VM when assigning an interface" msgstr "" -#: netbox/dcim/forms/bulk_import.py:1290 +#: netbox/dcim/forms/bulk_import.py:1308 msgid "Side A device" msgstr "" -#: netbox/dcim/forms/bulk_import.py:1293 netbox/dcim/forms/bulk_import.py:1311 +#: netbox/dcim/forms/bulk_import.py:1311 netbox/dcim/forms/bulk_import.py:1329 msgid "Device name" msgstr "" -#: netbox/dcim/forms/bulk_import.py:1296 +#: netbox/dcim/forms/bulk_import.py:1314 msgid "Side A type" msgstr "" -#: netbox/dcim/forms/bulk_import.py:1302 +#: netbox/dcim/forms/bulk_import.py:1320 msgid "Side A name" msgstr "" -#: netbox/dcim/forms/bulk_import.py:1303 netbox/dcim/forms/bulk_import.py:1321 +#: netbox/dcim/forms/bulk_import.py:1321 netbox/dcim/forms/bulk_import.py:1339 msgid "Termination name" msgstr "" -#: netbox/dcim/forms/bulk_import.py:1308 +#: netbox/dcim/forms/bulk_import.py:1326 msgid "Side B device" msgstr "" -#: netbox/dcim/forms/bulk_import.py:1314 +#: netbox/dcim/forms/bulk_import.py:1332 msgid "Side B type" msgstr "" -#: netbox/dcim/forms/bulk_import.py:1320 +#: netbox/dcim/forms/bulk_import.py:1338 msgid "Side B name" msgstr "" -#: netbox/dcim/forms/bulk_import.py:1329 +#: netbox/dcim/forms/bulk_import.py:1347 #: netbox/wireless/forms/bulk_import.py:91 msgid "Connection status" msgstr "" -#: netbox/dcim/forms/bulk_import.py:1381 +#: netbox/dcim/forms/bulk_import.py:1399 #, python-brace-format msgid "Side {side_upper}: {device} {termination_object} is already connected" msgstr "" -#: netbox/dcim/forms/bulk_import.py:1387 +#: netbox/dcim/forms/bulk_import.py:1405 #, python-brace-format msgid "{side_upper} side termination not found: {device} {name}" msgstr "" -#: netbox/dcim/forms/bulk_import.py:1412 netbox/dcim/forms/model_forms.py:797 +#: netbox/dcim/forms/bulk_import.py:1430 netbox/dcim/forms/model_forms.py:797 #: netbox/dcim/tables/devices.py:1058 netbox/templates/dcim/device.html:132 #: netbox/templates/dcim/virtualchassis.html:27 #: netbox/templates/dcim/virtualchassis.html:67 msgid "Master" msgstr "" -#: netbox/dcim/forms/bulk_import.py:1416 +#: netbox/dcim/forms/bulk_import.py:1434 msgid "Master device" msgstr "" -#: netbox/dcim/forms/bulk_import.py:1433 +#: netbox/dcim/forms/bulk_import.py:1451 msgid "Name of parent site" msgstr "" -#: netbox/dcim/forms/bulk_import.py:1467 +#: netbox/dcim/forms/bulk_import.py:1485 msgid "Upstream power panel" msgstr "" -#: netbox/dcim/forms/bulk_import.py:1497 +#: netbox/dcim/forms/bulk_import.py:1515 msgid "Primary or redundant" msgstr "" -#: netbox/dcim/forms/bulk_import.py:1502 +#: netbox/dcim/forms/bulk_import.py:1520 msgid "Supply type (AC/DC)" msgstr "" -#: netbox/dcim/forms/bulk_import.py:1507 +#: netbox/dcim/forms/bulk_import.py:1525 msgid "Single or three-phase" msgstr "" -#: netbox/dcim/forms/bulk_import.py:1558 netbox/dcim/forms/model_forms.py:1722 +#: netbox/dcim/forms/bulk_import.py:1576 netbox/dcim/forms/model_forms.py:1722 #: netbox/templates/dcim/device.html:190 #: netbox/templates/dcim/virtualdevicecontext.html:30 #: netbox/templates/virtualization/virtualmachine.html:52 msgid "Primary IPv4" msgstr "" -#: netbox/dcim/forms/bulk_import.py:1562 +#: netbox/dcim/forms/bulk_import.py:1580 msgid "IPv4 address with mask, e.g. 1.2.3.4/24" msgstr "" -#: netbox/dcim/forms/bulk_import.py:1565 netbox/dcim/forms/model_forms.py:1731 +#: netbox/dcim/forms/bulk_import.py:1583 netbox/dcim/forms/model_forms.py:1731 #: netbox/templates/dcim/device.html:206 #: netbox/templates/dcim/virtualdevicecontext.html:41 #: netbox/templates/virtualization/virtualmachine.html:68 msgid "Primary IPv6" msgstr "" -#: netbox/dcim/forms/bulk_import.py:1569 +#: netbox/dcim/forms/bulk_import.py:1587 msgid "IPv6 address with prefix length, e.g. 2001:db8::1/64" msgstr "" From d7b9b09d560767a1f7922af8852314c739802a09 Mon Sep 17 00:00:00 2001 From: Renato Almeida de Oliveira Date: Thu, 20 Mar 2025 10:07:48 -0300 Subject: [PATCH 10/15] Fixes: #18939 Allow ASN search by site-group (#18948) * Add site_group and site_group_id to ASNFilterSet and ASNFilterForm * Implement ASNTestCase site_group test case --- netbox/ipam/filtersets.py | 13 +++++++++++++ netbox/ipam/forms/filtersets.py | 7 ++++++- netbox/ipam/tests/test_filtersets.py | 21 ++++++++++++++++++--- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/netbox/ipam/filtersets.py b/netbox/ipam/filtersets.py index b23322a22..5c8dbc780 100644 --- a/netbox/ipam/filtersets.py +++ b/netbox/ipam/filtersets.py @@ -232,6 +232,19 @@ class ASNFilterSet(OrganizationalModelFilterSet, TenancyFilterSet): to_field_name='slug', label=_('RIR (slug)'), ) + site_group_id = TreeNodeMultipleChoiceFilter( + queryset=SiteGroup.objects.all(), + field_name='sites__group', + lookup_expr='in', + label=_('Site group (ID)'), + ) + site_group = TreeNodeMultipleChoiceFilter( + queryset=SiteGroup.objects.all(), + field_name='sites__group', + lookup_expr='in', + to_field_name='slug', + label=_('Site group (slug)'), + ) site_id = django_filters.ModelMultipleChoiceFilter( field_name='sites', queryset=Site.objects.all(), diff --git a/netbox/ipam/forms/filtersets.py b/netbox/ipam/forms/filtersets.py index a4faa18ed..183636148 100644 --- a/netbox/ipam/forms/filtersets.py +++ b/netbox/ipam/forms/filtersets.py @@ -142,7 +142,7 @@ class ASNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = ASN fieldsets = ( FieldSet('q', 'filter_id', 'tag'), - FieldSet('rir_id', 'site_id', name=_('Assignment')), + FieldSet('rir_id', 'site_group_id', 'site_id', name=_('Assignment')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), ) rir_id = DynamicModelMultipleChoiceField( @@ -150,6 +150,11 @@ class ASNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): required=False, label=_('RIR') ) + site_group_id = DynamicModelMultipleChoiceField( + queryset=SiteGroup.objects.all(), + required=False, + label=_('Site group') + ) site_id = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), required=False, diff --git a/netbox/ipam/tests/test_filtersets.py b/netbox/ipam/tests/test_filtersets.py index 5455beb9c..6281f7b41 100644 --- a/netbox/ipam/tests/test_filtersets.py +++ b/netbox/ipam/tests/test_filtersets.py @@ -133,10 +133,18 @@ class ASNTestCase(TestCase, ChangeLoggedFilterSetTests): ) ASN.objects.bulk_create(asns) + site_groups = ( + SiteGroup(name='Site Group 1', slug='site-group-1'), + SiteGroup(name='Site Group 2', slug='site-group-2'), + SiteGroup(name='Site Group 3', slug='site-group-3'), + ) + for site_group in site_groups: + site_group.save() + sites = [ - Site(name='Site 1', slug='site-1'), - Site(name='Site 2', slug='site-2'), - Site(name='Site 3', slug='site-3') + Site(name='Site 1', slug='site-1', group=site_groups[0]), + Site(name='Site 2', slug='site-2', group=site_groups[1]), + Site(name='Site 3', slug='site-3', group=site_groups[2]), ] Site.objects.bulk_create(sites) asns[0].sites.set([sites[0]]) @@ -178,6 +186,13 @@ class ASNTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'rir': [rirs[0].slug, rirs[1].slug]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + def test_site_group(self): + site_groups = SiteGroup.objects.all()[:2] + params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + params = {'site_group': [site_groups[0].slug, site_groups[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + def test_site(self): sites = Site.objects.all()[:2] params = {'site_id': [sites[0].pk, sites[1].pk]} From f07e2dd4e280cf830898d99754b26932eee86b5f Mon Sep 17 00:00:00 2001 From: Jason Novinger Date: Thu, 20 Mar 2025 08:12:05 -0500 Subject: [PATCH 11/15] Fixes #18944: Clearing widget type field no longer causes 500 error (#18946) * Fixes #18944: Clearing widget type field no longer causes 500 error * Address PR feedback, cleanup implementation --- netbox/extras/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 9b0eaebae..0ec7ae2af 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -1098,8 +1098,8 @@ class DashboardWidgetAddView(LoginRequiredMixin, View): if not request.htmx: return redirect('home') - initial = request.GET or { - 'widget_class': 'extras.NoteWidget', + initial = { + 'widget_class': request.GET.get('widget_class') or 'extras.NoteWidget', } widget_form = DashboardWidgetAddForm(initial=initial) widget_name = get_field_value(widget_form, 'widget_class') From d332a0c0d722cdfd4fb542e89381d1ecaf1d9e77 Mon Sep 17 00:00:00 2001 From: Renato Almeida de Oliveira Date: Thu, 20 Mar 2025 13:30:39 -0300 Subject: [PATCH 12/15] Fix sitegroup typo, VLANGroupFilterForm field didn't match between form and filterset (#18947) --- netbox/ipam/forms/filtersets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/ipam/forms/filtersets.py b/netbox/ipam/forms/filtersets.py index 183636148..e51ae6dae 100644 --- a/netbox/ipam/forms/filtersets.py +++ b/netbox/ipam/forms/filtersets.py @@ -423,7 +423,7 @@ class FHRPGroupFilterForm(NetBoxModelFilterSetForm): class VLANGroupFilterForm(NetBoxModelFilterSetForm): fieldsets = ( FieldSet('q', 'filter_id', 'tag'), - FieldSet('region', 'sitegroup', 'site', 'location', 'rack', name=_('Location')), + FieldSet('region', 'site_group', 'site', 'location', 'rack', name=_('Location')), FieldSet('cluster_group', 'cluster', name=_('Cluster')), FieldSet('contains_vid', name=_('VLANs')), ) @@ -433,7 +433,7 @@ class VLANGroupFilterForm(NetBoxModelFilterSetForm): required=False, label=_('Region') ) - sitegroup = DynamicModelMultipleChoiceField( + site_group = DynamicModelMultipleChoiceField( queryset=SiteGroup.objects.all(), required=False, label=_('Site group') From b8cc2d7116106a8ade9471fffe9416501e3c8bb8 Mon Sep 17 00:00:00 2001 From: Jason Novinger Date: Thu, 20 Mar 2025 15:55:12 -0500 Subject: [PATCH 13/15] Fixes #18887: Allows VMInterface object custom field on Prefix (#18945) --- .../api/serializers_/virtualmachines.py | 33 +++++++++++---- netbox/virtualization/tests/test_api.py | 41 ++++++++++++++++++- 2 files changed, 64 insertions(+), 10 deletions(-) diff --git a/netbox/virtualization/api/serializers_/virtualmachines.py b/netbox/virtualization/api/serializers_/virtualmachines.py index 05fa2e427..ed14b0a29 100644 --- a/netbox/virtualization/api/serializers_/virtualmachines.py +++ b/netbox/virtualization/api/serializers_/virtualmachines.py @@ -112,15 +112,32 @@ class VMInterfaceSerializer(NetBoxModelSerializer): brief_fields = ('id', 'url', 'display', 'virtual_machine', 'name', 'description') def validate(self, data): - # Validate many-to-many VLAN assignments - virtual_machine = self.instance.virtual_machine if self.instance else data.get('virtual_machine') - for vlan in data.get('tagged_vlans', []): - if vlan.site not in [virtual_machine.site, None]: - raise serializers.ValidationError({ - 'tagged_vlans': f"VLAN {vlan} must belong to the same site as the interface's parent virtual " - f"machine, or it must be global." - }) + virtual_machine = None + tagged_vlans = [] + + # #18887 + # There seem to be multiple code paths coming through here. Previously, we might either get + # the VirtualMachine instance from self.instance or from incoming data. However, #18887 + # illustrated that this is also being called when a custom field pointing to an object_type + # of VMInterface is on the right side of a custom-field assignment coming in from an API + # request. As such, we need to check a third way to access the VirtualMachine + # instance--where `data` is the VMInterface instance itself and we can get the associated + # VirtualMachine via attribute access. + if isinstance(data, dict): + virtual_machine = self.instance.virtual_machine if self.instance else data.get('virtual_machine') + tagged_vlans = data.get('tagged_vlans', []) + elif isinstance(data, VMInterface): + virtual_machine = data.virtual_machine + tagged_vlans = data.tagged_vlans.all() + + if virtual_machine: + for vlan in tagged_vlans: + if vlan.site not in [virtual_machine.site, None]: + raise serializers.ValidationError({ + 'tagged_vlans': f"VLAN {vlan} must belong to the same site as the interface's parent virtual " + f"machine, or it must be global." + }) return super().validate(data) diff --git a/netbox/virtualization/tests/test_api.py b/netbox/virtualization/tests/test_api.py index c57b57f2e..dfa8309a0 100644 --- a/netbox/virtualization/tests/test_api.py +++ b/netbox/virtualization/tests/test_api.py @@ -1,11 +1,15 @@ +from django.test import tag from django.urls import reverse +from netaddr import IPNetwork from rest_framework import status +from core.models import ObjectType from dcim.choices import InterfaceModeChoices from dcim.models import Site -from extras.models import ConfigTemplate +from extras.choices import CustomFieldTypeChoices +from extras.models import ConfigTemplate, CustomField from ipam.choices import VLANQinQRoleChoices -from ipam.models import VLAN, VRF +from ipam.models import Prefix, VLAN, VRF from utilities.testing import APITestCase, APIViewTestCases, create_test_device, create_test_virtualmachine from virtualization.choices import * from virtualization.models import * @@ -350,6 +354,39 @@ class VMInterfaceTest(APIViewTestCases.APIViewTestCase): }, ] + @tag('regression') + def test_set_vminterface_as_object_in_custom_field(self): + cf = CustomField.objects.create( + name='associated_interface', + type=CustomFieldTypeChoices.TYPE_OBJECT, + related_object_type=ObjectType.objects.get_for_model(VMInterface), + required=False + ) + cf.object_types.set([ObjectType.objects.get_for_model(Prefix)]) + cf.save() + + prefix = Prefix.objects.create(prefix=IPNetwork('10.0.0.0/12')) + vmi = VMInterface.objects.first() + + url = reverse('ipam-api:prefix-detail', kwargs={'pk': prefix.pk}) + data = { + 'custom_fields': { + 'associated_interface': vmi.id, + }, + } + + self.add_permissions('ipam.change_prefix') + + response = self.client.patch(url, data, format='json', **self.header) + self.assertEqual(response.status_code, 200) + + prefix_data = response.json() + self.assertEqual(prefix_data['custom_fields']['associated_interface']['id'], vmi.id) + + reloaded_prefix = Prefix.objects.get(pk=prefix.pk) + self.assertEqual(prefix.pk, reloaded_prefix.pk) + self.assertNotEqual(reloaded_prefix.cf['associated_interface'], None) + def test_bulk_delete_child_interfaces(self): interface1 = VMInterface.objects.get(name='Interface 1') virtual_machine = interface1.virtual_machine From 7db0765ed2bf7098bab17cb71e389ffbfde34ceb Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 21 Mar 2025 05:02:02 +0000 Subject: [PATCH 14/15] Update source translation strings --- netbox/translations/en/LC_MESSAGES/django.po | 270 ++++++++++--------- 1 file changed, 136 insertions(+), 134 deletions(-) diff --git a/netbox/translations/en/LC_MESSAGES/django.po b/netbox/translations/en/LC_MESSAGES/django.po index c308d5fc9..a97757095 100644 --- a/netbox/translations/en/LC_MESSAGES/django.po +++ b/netbox/translations/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-03-20 05:01+0000\n" +"POT-Creation-Date: 2025-03-21 05:01+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -164,7 +164,7 @@ msgstr "" #: netbox/dcim/filtersets.py:465 netbox/dcim/filtersets.py:1022 #: netbox/dcim/filtersets.py:1370 netbox/dcim/filtersets.py:2027 #: netbox/dcim/filtersets.py:2270 netbox/dcim/filtersets.py:2328 -#: netbox/ipam/filtersets.py:929 netbox/virtualization/filtersets.py:139 +#: netbox/ipam/filtersets.py:942 netbox/virtualization/filtersets.py:139 #: netbox/vpn/filtersets.py:358 msgid "Region (ID)" msgstr "" @@ -176,7 +176,7 @@ msgstr "" #: netbox/dcim/filtersets.py:472 netbox/dcim/filtersets.py:1029 #: netbox/dcim/filtersets.py:1377 netbox/dcim/filtersets.py:2034 #: netbox/dcim/filtersets.py:2277 netbox/dcim/filtersets.py:2335 -#: netbox/extras/filtersets.py:509 netbox/ipam/filtersets.py:936 +#: netbox/extras/filtersets.py:509 netbox/ipam/filtersets.py:949 #: netbox/virtualization/filtersets.py:146 netbox/vpn/filtersets.py:353 msgid "Region (slug)" msgstr "" @@ -187,8 +187,8 @@ msgstr "" #: netbox/dcim/filtersets.py:347 netbox/dcim/filtersets.py:478 #: netbox/dcim/filtersets.py:1035 netbox/dcim/filtersets.py:1383 #: netbox/dcim/filtersets.py:2040 netbox/dcim/filtersets.py:2283 -#: netbox/dcim/filtersets.py:2341 netbox/ipam/filtersets.py:942 -#: netbox/virtualization/filtersets.py:152 +#: netbox/dcim/filtersets.py:2341 netbox/ipam/filtersets.py:239 +#: netbox/ipam/filtersets.py:955 netbox/virtualization/filtersets.py:152 msgid "Site group (ID)" msgstr "" @@ -199,7 +199,8 @@ msgstr "" #: netbox/dcim/filtersets.py:1042 netbox/dcim/filtersets.py:1390 #: netbox/dcim/filtersets.py:2047 netbox/dcim/filtersets.py:2290 #: netbox/dcim/filtersets.py:2348 netbox/extras/filtersets.py:515 -#: netbox/ipam/filtersets.py:949 netbox/virtualization/filtersets.py:159 +#: netbox/ipam/filtersets.py:246 netbox/ipam/filtersets.py:962 +#: netbox/virtualization/filtersets.py:159 msgid "Site group (slug)" msgstr "" @@ -225,8 +226,8 @@ msgstr "" #: netbox/dcim/tables/racks.py:121 netbox/dcim/tables/racks.py:206 #: netbox/dcim/tables/sites.py:133 netbox/extras/filtersets.py:525 #: netbox/ipam/forms/bulk_edit.py:468 netbox/ipam/forms/bulk_import.py:459 -#: netbox/ipam/forms/filtersets.py:156 netbox/ipam/forms/filtersets.py:231 -#: netbox/ipam/forms/filtersets.py:439 netbox/ipam/forms/filtersets.py:534 +#: netbox/ipam/forms/filtersets.py:161 netbox/ipam/forms/filtersets.py:236 +#: netbox/ipam/forms/filtersets.py:444 netbox/ipam/forms/filtersets.py:539 #: netbox/ipam/forms/model_forms.py:679 netbox/ipam/tables/vlans.py:87 #: netbox/ipam/tables/vlans.py:197 netbox/templates/dcim/device.html:22 #: netbox/templates/dcim/inc/cable_termination.html:8 @@ -255,7 +256,7 @@ msgstr "" #: netbox/circuits/filtersets.py:315 netbox/dcim/base_filtersets.py:53 #: netbox/dcim/filtersets.py:243 netbox/dcim/filtersets.py:364 #: netbox/dcim/filtersets.py:459 netbox/extras/filtersets.py:531 -#: netbox/ipam/filtersets.py:244 netbox/ipam/filtersets.py:959 +#: netbox/ipam/filtersets.py:257 netbox/ipam/filtersets.py:972 #: netbox/virtualization/filtersets.py:169 netbox/vpn/filtersets.py:363 msgid "Site (slug)" msgstr "" @@ -274,14 +275,14 @@ msgstr "" #: netbox/circuits/filtersets.py:101 netbox/circuits/filtersets.py:128 #: netbox/circuits/filtersets.py:162 netbox/circuits/filtersets.py:338 #: netbox/circuits/filtersets.py:406 netbox/circuits/filtersets.py:482 -#: netbox/circuits/filtersets.py:550 netbox/ipam/filtersets.py:249 +#: netbox/circuits/filtersets.py:550 netbox/ipam/filtersets.py:262 msgid "Provider (ID)" msgstr "" #: netbox/circuits/filtersets.py:107 netbox/circuits/filtersets.py:134 #: netbox/circuits/filtersets.py:168 netbox/circuits/filtersets.py:344 #: netbox/circuits/filtersets.py:488 netbox/circuits/filtersets.py:556 -#: netbox/ipam/filtersets.py:255 +#: netbox/ipam/filtersets.py:268 msgid "Provider (slug)" msgstr "" @@ -313,8 +314,8 @@ msgstr "" #: netbox/dcim/filtersets.py:358 netbox/dcim/filtersets.py:453 #: netbox/dcim/filtersets.py:1046 netbox/dcim/filtersets.py:1395 #: netbox/dcim/filtersets.py:2052 netbox/dcim/filtersets.py:2294 -#: netbox/dcim/filtersets.py:2353 netbox/ipam/filtersets.py:238 -#: netbox/ipam/filtersets.py:953 netbox/virtualization/filtersets.py:163 +#: netbox/dcim/filtersets.py:2353 netbox/ipam/filtersets.py:251 +#: netbox/ipam/filtersets.py:966 netbox/virtualization/filtersets.py:163 #: netbox/vpn/filtersets.py:368 msgid "Site (ID)" msgstr "" @@ -435,7 +436,7 @@ msgid "Virtual circuit" msgstr "" #: netbox/circuits/filtersets.py:577 netbox/dcim/filtersets.py:1269 -#: netbox/dcim/filtersets.py:1634 netbox/ipam/filtersets.py:602 +#: netbox/dcim/filtersets.py:1634 netbox/ipam/filtersets.py:615 #: netbox/vpn/filtersets.py:102 netbox/vpn/filtersets.py:401 msgid "Interface (ID)" msgstr "" @@ -765,8 +766,8 @@ msgstr "" #: netbox/ipam/forms/bulk_edit.py:338 netbox/ipam/forms/bulk_edit.py:490 #: netbox/ipam/forms/bulk_import.py:195 netbox/ipam/forms/bulk_import.py:263 #: netbox/ipam/forms/bulk_import.py:299 netbox/ipam/forms/bulk_import.py:480 -#: netbox/ipam/forms/filtersets.py:214 netbox/ipam/forms/filtersets.py:287 -#: netbox/ipam/forms/filtersets.py:362 netbox/ipam/forms/filtersets.py:546 +#: netbox/ipam/forms/filtersets.py:219 netbox/ipam/forms/filtersets.py:292 +#: netbox/ipam/forms/filtersets.py:367 netbox/ipam/forms/filtersets.py:551 #: netbox/ipam/forms/model_forms.py:511 netbox/ipam/tables/ip.py:183 #: netbox/ipam/tables/ip.py:264 netbox/ipam/tables/ip.py:315 #: netbox/ipam/tables/ip.py:378 netbox/ipam/tables/ip.py:405 @@ -844,8 +845,8 @@ msgstr "" #: netbox/ipam/forms/bulk_import.py:473 netbox/ipam/forms/filtersets.py:50 #: netbox/ipam/forms/filtersets.py:70 netbox/ipam/forms/filtersets.py:102 #: netbox/ipam/forms/filtersets.py:123 netbox/ipam/forms/filtersets.py:146 -#: netbox/ipam/forms/filtersets.py:177 netbox/ipam/forms/filtersets.py:272 -#: netbox/ipam/forms/filtersets.py:316 netbox/ipam/forms/filtersets.py:514 +#: netbox/ipam/forms/filtersets.py:182 netbox/ipam/forms/filtersets.py:277 +#: netbox/ipam/forms/filtersets.py:321 netbox/ipam/forms/filtersets.py:519 #: netbox/ipam/tables/ip.py:408 netbox/ipam/tables/vlans.py:205 #: netbox/templates/circuits/circuit.html:48 #: netbox/templates/circuits/circuitgroup.html:36 @@ -948,11 +949,11 @@ msgstr "" #: netbox/extras/forms/filtersets.py:169 netbox/extras/forms/filtersets.py:210 #: netbox/extras/forms/filtersets.py:227 netbox/extras/forms/filtersets.py:258 #: netbox/extras/forms/filtersets.py:282 netbox/extras/forms/filtersets.py:449 -#: netbox/ipam/forms/filtersets.py:101 netbox/ipam/forms/filtersets.py:271 -#: netbox/ipam/forms/filtersets.py:313 netbox/ipam/forms/filtersets.py:389 -#: netbox/ipam/forms/filtersets.py:474 netbox/ipam/forms/filtersets.py:487 -#: netbox/ipam/forms/filtersets.py:512 netbox/ipam/forms/filtersets.py:583 -#: netbox/ipam/forms/filtersets.py:601 netbox/netbox/tables/tables.py:259 +#: netbox/ipam/forms/filtersets.py:101 netbox/ipam/forms/filtersets.py:276 +#: netbox/ipam/forms/filtersets.py:318 netbox/ipam/forms/filtersets.py:394 +#: netbox/ipam/forms/filtersets.py:479 netbox/ipam/forms/filtersets.py:492 +#: netbox/ipam/forms/filtersets.py:517 netbox/ipam/forms/filtersets.py:588 +#: netbox/ipam/forms/filtersets.py:606 netbox/netbox/tables/tables.py:259 #: netbox/virtualization/forms/filtersets.py:45 #: netbox/virtualization/forms/filtersets.py:108 #: netbox/virtualization/forms/filtersets.py:203 @@ -1076,9 +1077,9 @@ msgstr "" #: netbox/ipam/forms/bulk_edit.py:295 netbox/ipam/forms/bulk_edit.py:343 #: netbox/ipam/forms/bulk_edit.py:495 netbox/ipam/forms/bulk_import.py:200 #: netbox/ipam/forms/bulk_import.py:268 netbox/ipam/forms/bulk_import.py:304 -#: netbox/ipam/forms/bulk_import.py:485 netbox/ipam/forms/filtersets.py:242 -#: netbox/ipam/forms/filtersets.py:295 netbox/ipam/forms/filtersets.py:367 -#: netbox/ipam/forms/filtersets.py:554 netbox/ipam/forms/model_forms.py:194 +#: netbox/ipam/forms/bulk_import.py:485 netbox/ipam/forms/filtersets.py:247 +#: netbox/ipam/forms/filtersets.py:300 netbox/ipam/forms/filtersets.py:372 +#: netbox/ipam/forms/filtersets.py:559 netbox/ipam/forms/model_forms.py:194 #: netbox/ipam/forms/model_forms.py:220 netbox/ipam/forms/model_forms.py:259 #: netbox/ipam/forms/model_forms.py:686 netbox/ipam/tables/ip.py:209 #: netbox/ipam/tables/ip.py:268 netbox/ipam/tables/ip.py:319 @@ -1240,8 +1241,8 @@ msgstr "" #: netbox/dcim/tables/devices.py:167 netbox/dcim/tables/power.py:30 #: netbox/dcim/tables/racks.py:117 netbox/dcim/tables/racks.py:211 #: netbox/extras/filtersets.py:536 netbox/extras/forms/filtersets.py:327 -#: netbox/ipam/forms/filtersets.py:236 netbox/ipam/forms/filtersets.py:421 -#: netbox/ipam/forms/filtersets.py:444 netbox/ipam/forms/filtersets.py:511 +#: netbox/ipam/forms/filtersets.py:241 netbox/ipam/forms/filtersets.py:426 +#: netbox/ipam/forms/filtersets.py:449 netbox/ipam/forms/filtersets.py:516 #: netbox/templates/dcim/device.html:26 #: netbox/templates/dcim/device_edit.html:30 #: netbox/templates/dcim/inc/cable_termination.html:12 @@ -1263,9 +1264,9 @@ msgstr "" #: netbox/dcim/forms/filtersets.py:207 netbox/dcim/forms/filtersets.py:329 #: netbox/dcim/forms/filtersets.py:401 netbox/dcim/forms/filtersets.py:472 #: netbox/dcim/forms/filtersets.py:724 netbox/dcim/forms/filtersets.py:1092 -#: netbox/ipam/forms/filtersets.py:103 netbox/ipam/forms/filtersets.py:178 -#: netbox/ipam/forms/filtersets.py:273 netbox/ipam/forms/filtersets.py:318 -#: netbox/ipam/forms/filtersets.py:603 netbox/netbox/navigation/menu.py:31 +#: netbox/ipam/forms/filtersets.py:103 netbox/ipam/forms/filtersets.py:183 +#: netbox/ipam/forms/filtersets.py:278 netbox/ipam/forms/filtersets.py:323 +#: netbox/ipam/forms/filtersets.py:608 netbox/netbox/navigation/menu.py:31 #: netbox/netbox/navigation/menu.py:33 netbox/tenancy/forms/filtersets.py:42 #: netbox/tenancy/tables/columns.py:55 netbox/tenancy/tables/contacts.py:25 #: netbox/tenancy/views.py:19 netbox/virtualization/forms/filtersets.py:37 @@ -1291,8 +1292,8 @@ msgstr "" #: netbox/dcim/forms/model_forms.py:114 netbox/dcim/forms/object_create.py:367 #: netbox/dcim/tables/devices.py:153 netbox/dcim/tables/sites.py:85 #: netbox/extras/filtersets.py:503 netbox/ipam/forms/bulk_edit.py:458 -#: netbox/ipam/forms/filtersets.py:221 netbox/ipam/forms/filtersets.py:429 -#: netbox/ipam/forms/filtersets.py:520 netbox/templates/dcim/device.html:18 +#: netbox/ipam/forms/filtersets.py:226 netbox/ipam/forms/filtersets.py:434 +#: netbox/ipam/forms/filtersets.py:525 netbox/templates/dcim/device.html:18 #: netbox/templates/dcim/rack.html:16 #: netbox/templates/dcim/rackreservation.html:22 #: netbox/templates/dcim/region.html:26 netbox/templates/dcim/site.html:31 @@ -1313,8 +1314,9 @@ msgstr "" #: netbox/dcim/forms/filtersets.py:745 netbox/dcim/forms/filtersets.py:989 #: netbox/dcim/forms/filtersets.py:1103 netbox/dcim/forms/filtersets.py:1142 #: netbox/dcim/forms/object_create.py:375 netbox/extras/filtersets.py:520 -#: netbox/ipam/forms/bulk_edit.py:463 netbox/ipam/forms/filtersets.py:226 -#: netbox/ipam/forms/filtersets.py:434 netbox/ipam/forms/filtersets.py:525 +#: netbox/ipam/forms/bulk_edit.py:463 netbox/ipam/forms/filtersets.py:156 +#: netbox/ipam/forms/filtersets.py:231 netbox/ipam/forms/filtersets.py:439 +#: netbox/ipam/forms/filtersets.py:530 #: netbox/virtualization/forms/filtersets.py:64 #: netbox/virtualization/forms/filtersets.py:143 #: netbox/virtualization/forms/model_forms.py:98 @@ -1337,7 +1339,7 @@ msgstr "" #: netbox/circuits/forms/filtersets.py:287 netbox/dcim/forms/bulk_edit.py:1572 #: netbox/extras/forms/model_forms.py:596 netbox/ipam/forms/filtersets.py:145 -#: netbox/ipam/forms/filtersets.py:602 netbox/ipam/forms/model_forms.py:337 +#: netbox/ipam/forms/filtersets.py:607 netbox/ipam/forms/model_forms.py:337 #: netbox/templates/dcim/macaddress.html:25 #: netbox/templates/extras/configcontext.html:60 #: netbox/templates/ipam/ipaddress.html:59 @@ -1351,7 +1353,7 @@ msgstr "" #: netbox/circuits/tables/circuits.py:191 netbox/dcim/forms/bulk_edit.py:121 #: netbox/dcim/forms/bulk_import.py:102 netbox/dcim/forms/model_forms.py:120 #: netbox/dcim/tables/sites.py:89 netbox/extras/forms/filtersets.py:489 -#: netbox/ipam/filtersets.py:969 netbox/ipam/forms/bulk_edit.py:477 +#: netbox/ipam/filtersets.py:982 netbox/ipam/forms/bulk_edit.py:477 #: netbox/ipam/forms/bulk_import.py:466 netbox/ipam/forms/model_forms.py:571 #: netbox/ipam/tables/fhrp.py:67 netbox/ipam/tables/vlans.py:91 #: netbox/ipam/tables/vlans.py:202 @@ -1700,8 +1702,8 @@ msgstr "" #: netbox/extras/tables/tables.py:361 netbox/extras/tables/tables.py:378 #: netbox/extras/tables/tables.py:401 netbox/extras/tables/tables.py:439 #: netbox/extras/tables/tables.py:491 netbox/extras/tables/tables.py:514 -#: netbox/ipam/forms/bulk_edit.py:391 netbox/ipam/forms/filtersets.py:393 -#: netbox/ipam/forms/filtersets.py:478 netbox/ipam/tables/asn.py:16 +#: netbox/ipam/forms/bulk_edit.py:391 netbox/ipam/forms/filtersets.py:398 +#: netbox/ipam/forms/filtersets.py:483 netbox/ipam/tables/asn.py:16 #: netbox/ipam/tables/ip.py:31 netbox/ipam/tables/ip.py:106 #: netbox/ipam/tables/services.py:15 netbox/ipam/tables/services.py:40 #: netbox/ipam/tables/vlans.py:33 netbox/ipam/tables/vlans.py:83 @@ -1939,7 +1941,7 @@ msgstr "" #: netbox/dcim/tables/devices.py:891 netbox/dcim/tables/devices.py:959 #: netbox/dcim/tables/devices.py:1088 netbox/dcim/tables/modules.py:53 #: netbox/extras/forms/filtersets.py:328 netbox/ipam/forms/bulk_import.py:310 -#: netbox/ipam/forms/bulk_import.py:547 netbox/ipam/forms/filtersets.py:608 +#: netbox/ipam/forms/bulk_import.py:547 netbox/ipam/forms/filtersets.py:613 #: netbox/ipam/forms/model_forms.py:333 netbox/ipam/forms/model_forms.py:762 #: netbox/ipam/forms/model_forms.py:795 netbox/ipam/forms/model_forms.py:821 #: netbox/ipam/tables/vlans.py:156 @@ -2812,7 +2814,7 @@ msgstr "" msgid "Host" msgstr "" -#: netbox/core/tables/tasks.py:50 netbox/ipam/forms/filtersets.py:591 +#: netbox/core/tables/tasks.py:50 netbox/ipam/forms/filtersets.py:596 msgid "Port" msgstr "" @@ -3350,7 +3352,7 @@ msgid "Parent site group (slug)" msgstr "" #: netbox/dcim/filtersets.py:165 netbox/extras/filtersets.py:364 -#: netbox/ipam/filtersets.py:811 netbox/ipam/filtersets.py:963 +#: netbox/ipam/filtersets.py:824 netbox/ipam/filtersets.py:976 msgid "Group (ID)" msgstr "" @@ -3396,15 +3398,15 @@ msgstr "" #: netbox/dcim/filtersets.py:412 netbox/dcim/filtersets.py:893 #: netbox/dcim/filtersets.py:995 netbox/dcim/filtersets.py:1970 -#: netbox/ipam/filtersets.py:351 netbox/ipam/filtersets.py:463 -#: netbox/ipam/filtersets.py:973 netbox/virtualization/filtersets.py:176 +#: netbox/ipam/filtersets.py:364 netbox/ipam/filtersets.py:476 +#: netbox/ipam/filtersets.py:986 netbox/virtualization/filtersets.py:176 msgid "Role (ID)" msgstr "" #: netbox/dcim/filtersets.py:418 netbox/dcim/filtersets.py:899 #: netbox/dcim/filtersets.py:1001 netbox/dcim/filtersets.py:1976 -#: netbox/extras/filtersets.py:558 netbox/ipam/filtersets.py:357 -#: netbox/ipam/filtersets.py:469 netbox/ipam/filtersets.py:979 +#: netbox/extras/filtersets.py:558 netbox/ipam/filtersets.py:370 +#: netbox/ipam/filtersets.py:482 netbox/ipam/filtersets.py:992 #: netbox/virtualization/filtersets.py:182 msgid "Role (slug)" msgstr "" @@ -3612,8 +3614,8 @@ msgid "Module bay (ID)" msgstr "" #: netbox/dcim/filtersets.py:1335 netbox/dcim/filtersets.py:1427 -#: netbox/dcim/filtersets.py:1613 netbox/ipam/filtersets.py:581 -#: netbox/ipam/filtersets.py:821 netbox/ipam/filtersets.py:1143 +#: netbox/dcim/filtersets.py:1613 netbox/ipam/filtersets.py:594 +#: netbox/ipam/filtersets.py:834 netbox/ipam/filtersets.py:1156 #: netbox/virtualization/filtersets.py:127 netbox/vpn/filtersets.py:379 msgid "Device (ID)" msgstr "" @@ -3623,8 +3625,8 @@ msgid "Rack (name)" msgstr "" #: netbox/dcim/filtersets.py:1433 netbox/dcim/filtersets.py:1608 -#: netbox/ipam/filtersets.py:576 netbox/ipam/filtersets.py:816 -#: netbox/ipam/filtersets.py:1149 netbox/vpn/filtersets.py:374 +#: netbox/ipam/filtersets.py:589 netbox/ipam/filtersets.py:829 +#: netbox/ipam/filtersets.py:1162 netbox/vpn/filtersets.py:374 msgid "Device (name)" msgstr "" @@ -3662,30 +3664,30 @@ msgstr "" msgid "Cable (ID)" msgstr "" -#: netbox/dcim/filtersets.py:1618 netbox/ipam/filtersets.py:586 -#: netbox/ipam/filtersets.py:826 netbox/ipam/filtersets.py:1159 +#: netbox/dcim/filtersets.py:1618 netbox/ipam/filtersets.py:599 +#: netbox/ipam/filtersets.py:839 netbox/ipam/filtersets.py:1172 #: netbox/vpn/filtersets.py:385 msgid "Virtual machine (name)" msgstr "" -#: netbox/dcim/filtersets.py:1623 netbox/ipam/filtersets.py:591 -#: netbox/ipam/filtersets.py:831 netbox/ipam/filtersets.py:1153 +#: netbox/dcim/filtersets.py:1623 netbox/ipam/filtersets.py:604 +#: netbox/ipam/filtersets.py:844 netbox/ipam/filtersets.py:1166 #: netbox/virtualization/filtersets.py:248 #: netbox/virtualization/filtersets.py:299 netbox/vpn/filtersets.py:390 msgid "Virtual machine (ID)" msgstr "" -#: netbox/dcim/filtersets.py:1629 netbox/ipam/filtersets.py:597 +#: netbox/dcim/filtersets.py:1629 netbox/ipam/filtersets.py:610 #: netbox/vpn/filtersets.py:97 netbox/vpn/filtersets.py:396 msgid "Interface (name)" msgstr "" -#: netbox/dcim/filtersets.py:1640 netbox/ipam/filtersets.py:608 +#: netbox/dcim/filtersets.py:1640 netbox/ipam/filtersets.py:621 #: netbox/vpn/filtersets.py:108 netbox/vpn/filtersets.py:407 msgid "VM interface (name)" msgstr "" -#: netbox/dcim/filtersets.py:1645 netbox/ipam/filtersets.py:613 +#: netbox/dcim/filtersets.py:1645 netbox/ipam/filtersets.py:626 #: netbox/vpn/filtersets.py:113 msgid "VM interface (ID)" msgstr "" @@ -3703,14 +3705,14 @@ msgstr "" #: netbox/dcim/forms/bulk_import.py:921 netbox/dcim/forms/filtersets.py:1433 #: netbox/dcim/forms/model_forms.py:1411 #: netbox/dcim/models/device_components.py:752 -#: netbox/dcim/tables/devices.py:647 netbox/ipam/filtersets.py:322 -#: netbox/ipam/filtersets.py:333 netbox/ipam/filtersets.py:453 -#: netbox/ipam/filtersets.py:554 netbox/ipam/filtersets.py:565 +#: netbox/dcim/tables/devices.py:647 netbox/ipam/filtersets.py:335 +#: netbox/ipam/filtersets.py:346 netbox/ipam/filtersets.py:466 +#: netbox/ipam/filtersets.py:567 netbox/ipam/filtersets.py:578 #: netbox/ipam/forms/bulk_edit.py:226 netbox/ipam/forms/bulk_edit.py:282 #: netbox/ipam/forms/bulk_edit.py:324 netbox/ipam/forms/bulk_import.py:160 #: netbox/ipam/forms/bulk_import.py:249 netbox/ipam/forms/bulk_import.py:285 -#: netbox/ipam/forms/filtersets.py:69 netbox/ipam/forms/filtersets.py:175 -#: netbox/ipam/forms/filtersets.py:315 netbox/ipam/forms/model_forms.py:65 +#: netbox/ipam/forms/filtersets.py:69 netbox/ipam/forms/filtersets.py:180 +#: netbox/ipam/forms/filtersets.py:320 netbox/ipam/forms/model_forms.py:65 #: netbox/ipam/forms/model_forms.py:208 netbox/ipam/forms/model_forms.py:256 #: netbox/ipam/forms/model_forms.py:310 netbox/ipam/forms/model_forms.py:474 #: netbox/ipam/forms/model_forms.py:488 netbox/ipam/forms/model_forms.py:502 @@ -3732,20 +3734,20 @@ msgstr "" msgid "VRF" msgstr "" -#: netbox/dcim/filtersets.py:1702 netbox/ipam/filtersets.py:328 -#: netbox/ipam/filtersets.py:339 netbox/ipam/filtersets.py:459 -#: netbox/ipam/filtersets.py:560 netbox/ipam/filtersets.py:571 +#: netbox/dcim/filtersets.py:1702 netbox/ipam/filtersets.py:341 +#: netbox/ipam/filtersets.py:352 netbox/ipam/filtersets.py:472 +#: netbox/ipam/filtersets.py:573 netbox/ipam/filtersets.py:584 msgid "VRF (RD)" msgstr "" -#: netbox/dcim/filtersets.py:1707 netbox/ipam/filtersets.py:1011 +#: netbox/dcim/filtersets.py:1707 netbox/ipam/filtersets.py:1024 #: netbox/vpn/filtersets.py:342 msgid "L2VPN (ID)" msgstr "" #: netbox/dcim/filtersets.py:1713 netbox/dcim/forms/filtersets.py:1438 -#: netbox/dcim/tables/devices.py:583 netbox/ipam/filtersets.py:1017 -#: netbox/ipam/forms/filtersets.py:574 netbox/ipam/tables/vlans.py:113 +#: netbox/dcim/tables/devices.py:583 netbox/ipam/filtersets.py:1030 +#: netbox/ipam/forms/filtersets.py:579 netbox/ipam/tables/vlans.py:113 #: netbox/templates/dcim/interface.html:99 netbox/templates/ipam/vlan.html:82 #: netbox/templates/vpn/l2vpntermination.html:12 #: netbox/virtualization/forms/filtersets.py:238 @@ -3755,13 +3757,13 @@ msgstr "" msgid "L2VPN" msgstr "" -#: netbox/dcim/filtersets.py:1718 netbox/ipam/filtersets.py:1092 +#: netbox/dcim/filtersets.py:1718 netbox/ipam/filtersets.py:1105 msgid "VLAN Translation Policy (ID)" msgstr "" #: netbox/dcim/filtersets.py:1724 netbox/dcim/forms/model_forms.py:1428 #: netbox/dcim/models/device_components.py:571 -#: netbox/ipam/forms/filtersets.py:493 netbox/ipam/forms/model_forms.py:712 +#: netbox/ipam/forms/filtersets.py:498 netbox/ipam/forms/model_forms.py:712 #: netbox/templates/ipam/vlantranslationpolicy.html:11 #: netbox/virtualization/forms/bulk_edit.py:248 #: netbox/virtualization/forms/model_forms.py:373 @@ -4097,7 +4099,7 @@ msgstr "" #: netbox/dcim/forms/model_forms.py:314 netbox/dcim/forms/model_forms.py:489 #: netbox/dcim/forms/model_forms.py:767 netbox/dcim/forms/object_create.py:392 #: netbox/dcim/tables/devices.py:171 netbox/dcim/tables/power.py:70 -#: netbox/dcim/tables/racks.py:216 netbox/ipam/forms/filtersets.py:449 +#: netbox/dcim/tables/racks.py:216 netbox/ipam/forms/filtersets.py:454 #: netbox/templates/dcim/device.html:30 #: netbox/templates/dcim/inc/cable_termination.html:16 #: netbox/templates/dcim/powerfeed.html:28 netbox/templates/dcim/rack.html:13 @@ -4204,7 +4206,7 @@ msgstr "" #: netbox/dcim/forms/filtersets.py:729 netbox/dcim/forms/filtersets.py:899 #: netbox/dcim/forms/model_forms.py:533 netbox/dcim/tables/devices.py:212 #: netbox/extras/filtersets.py:596 netbox/extras/forms/filtersets.py:329 -#: netbox/ipam/forms/filtersets.py:422 netbox/ipam/forms/filtersets.py:454 +#: netbox/ipam/forms/filtersets.py:427 netbox/ipam/forms/filtersets.py:459 #: netbox/templates/dcim/device.html:239 #: netbox/templates/virtualization/cluster.html:10 #: netbox/templates/virtualization/virtualmachine.html:92 @@ -4408,7 +4410,7 @@ msgid "Mode" msgstr "" #: netbox/dcim/forms/bulk_edit.py:1493 netbox/dcim/forms/model_forms.py:1377 -#: netbox/ipam/forms/bulk_import.py:174 netbox/ipam/forms/filtersets.py:543 +#: netbox/ipam/forms/bulk_import.py:174 netbox/ipam/forms/filtersets.py:548 #: netbox/ipam/models/vlans.py:86 netbox/virtualization/forms/bulk_edit.py:222 #: netbox/virtualization/forms/model_forms.py:335 msgid "VLAN group" @@ -4454,7 +4456,7 @@ msgstr "" #: netbox/dcim/forms/bulk_edit.py:1563 netbox/dcim/forms/filtersets.py:1333 #: netbox/dcim/forms/model_forms.py:1435 netbox/ipam/forms/bulk_edit.py:269 -#: netbox/ipam/forms/bulk_edit.py:362 netbox/ipam/forms/filtersets.py:172 +#: netbox/ipam/forms/bulk_edit.py:362 netbox/ipam/forms/filtersets.py:177 #: netbox/netbox/navigation/menu.py:108 #: netbox/templates/dcim/interface.html:128 #: netbox/templates/ipam/prefix.html:91 @@ -4789,8 +4791,8 @@ msgstr "" #: netbox/dcim/forms/bulk_import.py:925 netbox/ipam/forms/bulk_import.py:164 #: netbox/ipam/forms/bulk_import.py:253 netbox/ipam/forms/bulk_import.py:289 -#: netbox/ipam/forms/filtersets.py:205 netbox/ipam/forms/filtersets.py:283 -#: netbox/ipam/forms/filtersets.py:343 +#: netbox/ipam/forms/filtersets.py:210 netbox/ipam/forms/filtersets.py:288 +#: netbox/ipam/forms/filtersets.py:348 #: netbox/virtualization/forms/bulk_import.py:181 msgid "Assigned VRF" msgstr "" @@ -4890,7 +4892,7 @@ msgstr "" msgid "Parent VM of assigned interface (if any)" msgstr "" -#: netbox/dcim/forms/bulk_import.py:1241 netbox/ipam/filtersets.py:1022 +#: netbox/dcim/forms/bulk_import.py:1241 netbox/ipam/filtersets.py:1035 #: netbox/ipam/forms/bulk_import.py:328 msgid "Assigned interface" msgstr "" @@ -5116,7 +5118,7 @@ msgid "Has virtual device contexts" msgstr "" #: netbox/dcim/forms/filtersets.py:904 netbox/extras/filtersets.py:585 -#: netbox/ipam/forms/filtersets.py:459 +#: netbox/ipam/forms/filtersets.py:464 #: netbox/virtualization/forms/filtersets.py:117 msgid "Cluster group" msgstr "" @@ -5193,11 +5195,11 @@ msgstr "" msgid "Discovered" msgstr "" -#: netbox/dcim/forms/filtersets.py:1596 netbox/ipam/forms/filtersets.py:354 +#: netbox/dcim/forms/filtersets.py:1596 netbox/ipam/forms/filtersets.py:359 msgid "Assigned Device" msgstr "" -#: netbox/dcim/forms/filtersets.py:1601 netbox/ipam/forms/filtersets.py:359 +#: netbox/dcim/forms/filtersets.py:1601 netbox/ipam/forms/filtersets.py:364 msgid "Assigned VM" msgstr "" @@ -5213,7 +5215,7 @@ msgstr "" #: netbox/dcim/forms/mixins.py:30 netbox/dcim/forms/mixins.py:78 #: netbox/ipam/forms/bulk_edit.py:270 netbox/ipam/forms/bulk_edit.py:423 -#: netbox/ipam/forms/bulk_edit.py:437 netbox/ipam/forms/filtersets.py:176 +#: netbox/ipam/forms/bulk_edit.py:437 netbox/ipam/forms/filtersets.py:181 #: netbox/ipam/forms/model_forms.py:231 netbox/ipam/forms/model_forms.py:621 #: netbox/ipam/forms/model_forms.py:631 netbox/ipam/tables/ip.py:194 #: netbox/ipam/tables/vlans.py:40 netbox/templates/ipam/prefix.html:48 @@ -5436,7 +5438,7 @@ msgstr "" msgid "VM Interface" msgstr "" -#: netbox/dcim/forms/model_forms.py:1788 netbox/ipam/forms/filtersets.py:613 +#: netbox/dcim/forms/model_forms.py:1788 netbox/ipam/forms/filtersets.py:618 #: netbox/ipam/forms/model_forms.py:334 netbox/ipam/forms/model_forms.py:796 #: netbox/ipam/forms/model_forms.py:822 netbox/ipam/tables/vlans.py:171 #: netbox/templates/virtualization/virtualdisk.html:21 @@ -5972,7 +5974,7 @@ msgstr "" #: netbox/dcim/models/device_components.py:564 #: netbox/dcim/tables/devices.py:601 netbox/ipam/forms/bulk_edit.py:510 -#: netbox/ipam/forms/bulk_import.py:498 netbox/ipam/forms/filtersets.py:569 +#: netbox/ipam/forms/bulk_import.py:498 netbox/ipam/forms/filtersets.py:574 #: netbox/ipam/forms/model_forms.py:692 netbox/ipam/tables/vlans.py:106 #: netbox/templates/dcim/interface.html:86 netbox/templates/ipam/vlan.html:77 msgid "Q-in-Q SVLAN" @@ -9564,7 +9566,7 @@ msgstr "" msgid "Exporting L2VPN (identifier)" msgstr "" -#: netbox/ipam/filtersets.py:159 netbox/ipam/filtersets.py:287 +#: netbox/ipam/filtersets.py:159 netbox/ipam/filtersets.py:300 #: netbox/ipam/forms/model_forms.py:229 netbox/ipam/tables/ip.py:158 #: netbox/templates/ipam/prefix.html:12 msgid "Prefix" @@ -9580,96 +9582,96 @@ msgstr "" msgid "RIR (slug)" msgstr "" -#: netbox/ipam/filtersets.py:291 +#: netbox/ipam/filtersets.py:304 msgid "Within prefix" msgstr "" -#: netbox/ipam/filtersets.py:295 +#: netbox/ipam/filtersets.py:308 msgid "Within and including prefix" msgstr "" -#: netbox/ipam/filtersets.py:299 +#: netbox/ipam/filtersets.py:312 msgid "Prefixes which contain this prefix or IP" msgstr "" -#: netbox/ipam/filtersets.py:310 netbox/ipam/filtersets.py:542 -#: netbox/ipam/forms/bulk_edit.py:327 netbox/ipam/forms/filtersets.py:200 -#: netbox/ipam/forms/filtersets.py:338 +#: netbox/ipam/filtersets.py:323 netbox/ipam/filtersets.py:555 +#: netbox/ipam/forms/bulk_edit.py:327 netbox/ipam/forms/filtersets.py:205 +#: netbox/ipam/forms/filtersets.py:343 msgid "Mask length" msgstr "" -#: netbox/ipam/filtersets.py:343 netbox/vpn/filtersets.py:427 +#: netbox/ipam/filtersets.py:356 netbox/vpn/filtersets.py:427 msgid "VLAN (ID)" msgstr "" -#: netbox/ipam/filtersets.py:347 netbox/vpn/filtersets.py:422 +#: netbox/ipam/filtersets.py:360 netbox/vpn/filtersets.py:422 msgid "VLAN number (1-4094)" msgstr "" -#: netbox/ipam/filtersets.py:441 netbox/ipam/filtersets.py:445 -#: netbox/ipam/filtersets.py:537 netbox/ipam/forms/model_forms.py:506 +#: netbox/ipam/filtersets.py:454 netbox/ipam/filtersets.py:458 +#: netbox/ipam/filtersets.py:550 netbox/ipam/forms/model_forms.py:506 #: netbox/templates/tenancy/contact.html:53 #: netbox/tenancy/forms/bulk_edit.py:113 msgid "Address" msgstr "" -#: netbox/ipam/filtersets.py:449 +#: netbox/ipam/filtersets.py:462 msgid "Ranges which contain this prefix or IP" msgstr "" -#: netbox/ipam/filtersets.py:477 netbox/ipam/filtersets.py:533 +#: netbox/ipam/filtersets.py:490 netbox/ipam/filtersets.py:546 msgid "Parent prefix" msgstr "" -#: netbox/ipam/filtersets.py:618 +#: netbox/ipam/filtersets.py:631 msgid "FHRP group (ID)" msgstr "" -#: netbox/ipam/filtersets.py:622 +#: netbox/ipam/filtersets.py:635 msgid "Is assigned to an interface" msgstr "" -#: netbox/ipam/filtersets.py:626 +#: netbox/ipam/filtersets.py:639 msgid "Is assigned" msgstr "" -#: netbox/ipam/filtersets.py:638 +#: netbox/ipam/filtersets.py:651 msgid "Service (ID)" msgstr "" -#: netbox/ipam/filtersets.py:643 +#: netbox/ipam/filtersets.py:656 msgid "NAT inside IP address (ID)" msgstr "" -#: netbox/ipam/filtersets.py:1002 +#: netbox/ipam/filtersets.py:1015 msgid "Q-in-Q SVLAN (ID)" msgstr "" -#: netbox/ipam/filtersets.py:1006 +#: netbox/ipam/filtersets.py:1019 msgid "Q-in-Q SVLAN number (1-4094)" msgstr "" -#: netbox/ipam/filtersets.py:1027 +#: netbox/ipam/filtersets.py:1040 msgid "Assigned VM interface" msgstr "" -#: netbox/ipam/filtersets.py:1098 +#: netbox/ipam/filtersets.py:1111 msgid "VLAN Translation Policy (name)" msgstr "" -#: netbox/ipam/filtersets.py:1164 +#: netbox/ipam/filtersets.py:1177 msgid "IP address (ID)" msgstr "" -#: netbox/ipam/filtersets.py:1170 netbox/ipam/models/ip.py:788 +#: netbox/ipam/filtersets.py:1183 netbox/ipam/models/ip.py:788 msgid "IP address" msgstr "" -#: netbox/ipam/filtersets.py:1195 +#: netbox/ipam/filtersets.py:1208 msgid "Primary IPv4 (ID)" msgstr "" -#: netbox/ipam/filtersets.py:1200 +#: netbox/ipam/filtersets.py:1213 msgid "Primary IPv6 (ID)" msgstr "" @@ -9737,7 +9739,7 @@ msgid "VLAN Group" msgstr "" #: netbox/ipam/forms/bulk_edit.py:218 netbox/ipam/forms/bulk_import.py:188 -#: netbox/ipam/forms/filtersets.py:261 netbox/ipam/forms/model_forms.py:217 +#: netbox/ipam/forms/filtersets.py:266 netbox/ipam/forms/model_forms.py:217 #: netbox/ipam/models/vlans.py:272 netbox/ipam/tables/ip.py:206 #: netbox/templates/ipam/prefix.html:56 netbox/templates/ipam/vlan.html:12 #: netbox/templates/ipam/vlan/base.html:6 @@ -9755,18 +9757,18 @@ msgstr "" msgid "Prefix length" msgstr "" -#: netbox/ipam/forms/bulk_edit.py:252 netbox/ipam/forms/filtersets.py:246 +#: netbox/ipam/forms/bulk_edit.py:252 netbox/ipam/forms/filtersets.py:251 #: netbox/templates/ipam/prefix.html:81 msgid "Is a pool" msgstr "" #: netbox/ipam/forms/bulk_edit.py:257 netbox/ipam/forms/bulk_edit.py:302 -#: netbox/ipam/forms/filtersets.py:253 netbox/ipam/forms/filtersets.py:299 +#: netbox/ipam/forms/filtersets.py:258 netbox/ipam/forms/filtersets.py:304 #: netbox/ipam/models/ip.py:256 netbox/ipam/models/ip.py:525 msgid "Treat as fully utilized" msgstr "" -#: netbox/ipam/forms/bulk_edit.py:271 netbox/ipam/forms/filtersets.py:174 +#: netbox/ipam/forms/bulk_edit.py:271 netbox/ipam/forms/filtersets.py:179 #: netbox/ipam/forms/model_forms.py:232 msgid "VLAN Assignment" msgstr "" @@ -9777,20 +9779,20 @@ msgstr "" #: netbox/ipam/forms/bulk_edit.py:371 netbox/ipam/forms/bulk_edit.py:562 #: netbox/ipam/forms/bulk_import.py:424 netbox/ipam/forms/bulk_import.py:535 -#: netbox/ipam/forms/bulk_import.py:561 netbox/ipam/forms/filtersets.py:397 -#: netbox/ipam/forms/filtersets.py:586 netbox/templates/ipam/fhrpgroup.html:22 +#: netbox/ipam/forms/bulk_import.py:561 netbox/ipam/forms/filtersets.py:402 +#: netbox/ipam/forms/filtersets.py:591 netbox/templates/ipam/fhrpgroup.html:22 #: netbox/templates/ipam/inc/panels/fhrp_groups.html:24 #: netbox/templates/ipam/service.html:32 #: netbox/templates/ipam/servicetemplate.html:19 msgid "Protocol" msgstr "" -#: netbox/ipam/forms/bulk_edit.py:378 netbox/ipam/forms/filtersets.py:404 +#: netbox/ipam/forms/bulk_edit.py:378 netbox/ipam/forms/filtersets.py:409 #: netbox/ipam/tables/fhrp.py:22 netbox/templates/ipam/fhrpgroup.html:26 msgid "Group ID" msgstr "" -#: netbox/ipam/forms/bulk_edit.py:383 netbox/ipam/forms/filtersets.py:409 +#: netbox/ipam/forms/bulk_edit.py:383 netbox/ipam/forms/filtersets.py:414 #: netbox/wireless/forms/bulk_edit.py:70 netbox/wireless/forms/bulk_edit.py:118 #: netbox/wireless/forms/bulk_import.py:64 #: netbox/wireless/forms/bulk_import.py:67 @@ -9801,11 +9803,11 @@ msgstr "" msgid "Authentication type" msgstr "" -#: netbox/ipam/forms/bulk_edit.py:388 netbox/ipam/forms/filtersets.py:413 +#: netbox/ipam/forms/bulk_edit.py:388 netbox/ipam/forms/filtersets.py:418 msgid "Authentication key" msgstr "" -#: netbox/ipam/forms/bulk_edit.py:405 netbox/ipam/forms/filtersets.py:390 +#: netbox/ipam/forms/bulk_edit.py:405 netbox/ipam/forms/filtersets.py:395 #: netbox/ipam/forms/model_forms.py:517 netbox/netbox/navigation/menu.py:407 #: netbox/templates/ipam/fhrpgroup.html:49 #: netbox/templates/wireless/inc/authentication_attrs.html:5 @@ -9822,7 +9824,7 @@ msgid "VLAN ID ranges" msgstr "" #: netbox/ipam/forms/bulk_edit.py:505 netbox/ipam/forms/bulk_import.py:492 -#: netbox/ipam/forms/filtersets.py:561 netbox/ipam/models/vlans.py:232 +#: netbox/ipam/forms/filtersets.py:566 netbox/ipam/models/vlans.py:232 #: netbox/ipam/tables/vlans.py:103 msgid "Q-in-Q role" msgstr "" @@ -9973,8 +9975,8 @@ msgstr "" msgid "Private" msgstr "" -#: netbox/ipam/forms/filtersets.py:108 netbox/ipam/forms/filtersets.py:195 -#: netbox/ipam/forms/filtersets.py:278 netbox/ipam/forms/filtersets.py:333 +#: netbox/ipam/forms/filtersets.py:108 netbox/ipam/forms/filtersets.py:200 +#: netbox/ipam/forms/filtersets.py:283 netbox/ipam/forms/filtersets.py:338 msgid "Address family" msgstr "" @@ -9990,54 +9992,54 @@ msgstr "" msgid "End" msgstr "" -#: netbox/ipam/forms/filtersets.py:190 +#: netbox/ipam/forms/filtersets.py:195 msgid "Search within" msgstr "" -#: netbox/ipam/forms/filtersets.py:211 netbox/ipam/forms/filtersets.py:349 +#: netbox/ipam/forms/filtersets.py:216 netbox/ipam/forms/filtersets.py:354 msgid "Present in VRF" msgstr "" -#: netbox/ipam/forms/filtersets.py:317 +#: netbox/ipam/forms/filtersets.py:322 msgid "Device/VM" msgstr "" -#: netbox/ipam/forms/filtersets.py:328 +#: netbox/ipam/forms/filtersets.py:333 msgid "Parent Prefix" msgstr "" -#: netbox/ipam/forms/filtersets.py:373 +#: netbox/ipam/forms/filtersets.py:378 msgid "Assigned to an interface" msgstr "" -#: netbox/ipam/forms/filtersets.py:380 netbox/templates/ipam/ipaddress.html:51 +#: netbox/ipam/forms/filtersets.py:385 netbox/templates/ipam/ipaddress.html:51 msgid "DNS Name" msgstr "" -#: netbox/ipam/forms/filtersets.py:423 netbox/ipam/models/vlans.py:273 +#: netbox/ipam/forms/filtersets.py:428 netbox/ipam/models/vlans.py:273 #: netbox/ipam/tables/ip.py:122 netbox/ipam/tables/vlans.py:51 #: netbox/ipam/views.py:1036 netbox/netbox/navigation/menu.py:199 #: netbox/netbox/navigation/menu.py:201 msgid "VLANs" msgstr "" -#: netbox/ipam/forms/filtersets.py:464 +#: netbox/ipam/forms/filtersets.py:469 msgid "Contains VLAN ID" msgstr "" -#: netbox/ipam/forms/filtersets.py:498 netbox/ipam/models/vlans.py:363 +#: netbox/ipam/forms/filtersets.py:503 netbox/ipam/models/vlans.py:363 msgid "Local VLAN ID" msgstr "" -#: netbox/ipam/forms/filtersets.py:503 netbox/ipam/models/vlans.py:371 +#: netbox/ipam/forms/filtersets.py:508 netbox/ipam/models/vlans.py:371 msgid "Remote VLAN ID" msgstr "" -#: netbox/ipam/forms/filtersets.py:513 +#: netbox/ipam/forms/filtersets.py:518 msgid "Q-in-Q/802.1ad" msgstr "" -#: netbox/ipam/forms/filtersets.py:558 netbox/ipam/models/vlans.py:191 +#: netbox/ipam/forms/filtersets.py:563 netbox/ipam/models/vlans.py:191 #: netbox/templates/ipam/vlan.html:31 msgid "VLAN ID" msgstr "" From b1d014b5206494cd94c058f6e365bea63eca2eba Mon Sep 17 00:00:00 2001 From: Yi Date: Sat, 22 Mar 2025 02:00:50 +0800 Subject: [PATCH 15/15] Fixes #18949: Add missing GraphQL ContactsMixin in types with ContactAssignments --- netbox/circuits/graphql/types.py | 2 +- netbox/ipam/graphql/types.py | 11 ++++++----- netbox/tenancy/graphql/types.py | 4 ++-- netbox/virtualization/graphql/types.py | 4 ++-- netbox/vpn/graphql/types.py | 4 ++-- 5 files changed, 13 insertions(+), 12 deletions(-) diff --git a/netbox/circuits/graphql/types.py b/netbox/circuits/graphql/types.py index 564b5ed6f..96fcaa144 100644 --- a/netbox/circuits/graphql/types.py +++ b/netbox/circuits/graphql/types.py @@ -43,7 +43,7 @@ class ProviderType(NetBoxObjectType, ContactsMixin): fields='__all__', filters=ProviderAccountFilter ) -class ProviderAccountType(NetBoxObjectType): +class ProviderAccountType(ContactsMixin, NetBoxObjectType): provider: Annotated["ProviderType", strawberry.lazy('circuits.graphql.types')] circuits: List[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]] diff --git a/netbox/ipam/graphql/types.py b/netbox/ipam/graphql/types.py index e6ecca984..54ce2fc74 100644 --- a/netbox/ipam/graphql/types.py +++ b/netbox/ipam/graphql/types.py @@ -5,6 +5,7 @@ import strawberry_django from circuits.graphql.types import ProviderType from dcim.graphql.types import SiteType +from extras.graphql.mixins import ContactsMixin from ipam import models from netbox.graphql.scalars import BigInt from netbox.graphql.types import BaseObjectType, NetBoxObjectType, OrganizationalObjectType @@ -83,7 +84,7 @@ class ASNRangeType(NetBoxObjectType): fields='__all__', filters=AggregateFilter ) -class AggregateType(NetBoxObjectType, BaseIPAddressFamilyType): +class AggregateType(NetBoxObjectType, ContactsMixin, BaseIPAddressFamilyType): prefix: str rir: Annotated["RIRType", strawberry.lazy('ipam.graphql.types')] | None tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None @@ -120,7 +121,7 @@ class FHRPGroupAssignmentType(BaseObjectType): exclude=('assigned_object_type', 'assigned_object_id', 'address'), filters=IPAddressFilter ) -class IPAddressType(NetBoxObjectType, BaseIPAddressFamilyType): +class IPAddressType(NetBoxObjectType, ContactsMixin, BaseIPAddressFamilyType): address: str vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None @@ -144,7 +145,7 @@ class IPAddressType(NetBoxObjectType, BaseIPAddressFamilyType): fields='__all__', filters=IPRangeFilter ) -class IPRangeType(NetBoxObjectType): +class IPRangeType(NetBoxObjectType, ContactsMixin): start_address: str end_address: str vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None @@ -157,7 +158,7 @@ class IPRangeType(NetBoxObjectType): exclude=('scope_type', 'scope_id', '_location', '_region', '_site', '_site_group'), filters=PrefixFilter ) -class PrefixType(NetBoxObjectType, BaseIPAddressFamilyType): +class PrefixType(NetBoxObjectType, ContactsMixin, BaseIPAddressFamilyType): prefix: str vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None @@ -217,7 +218,7 @@ class RouteTargetType(NetBoxObjectType): fields='__all__', filters=ServiceFilter ) -class ServiceType(NetBoxObjectType): +class ServiceType(NetBoxObjectType, ContactsMixin): ports: List[int] device: Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')] | None virtual_machine: Annotated["VirtualMachineType", strawberry.lazy('virtualization.graphql.types')] | None diff --git a/netbox/tenancy/graphql/types.py b/netbox/tenancy/graphql/types.py index 7baa136b3..b07a14c80 100644 --- a/netbox/tenancy/graphql/types.py +++ b/netbox/tenancy/graphql/types.py @@ -3,7 +3,7 @@ from typing import Annotated, List import strawberry import strawberry_django -from extras.graphql.mixins import CustomFieldsMixin, TagsMixin +from extras.graphql.mixins import CustomFieldsMixin, TagsMixin, ContactsMixin from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, NetBoxObjectType from tenancy import models from .mixins import ContactAssignmentsMixin @@ -28,7 +28,7 @@ __all__ = ( fields='__all__', filters=TenantFilter ) -class TenantType(NetBoxObjectType): +class TenantType(ContactsMixin, NetBoxObjectType): group: Annotated["TenantGroupType", strawberry.lazy('tenancy.graphql.types')] | None asns: List[Annotated["ASNType", strawberry.lazy('ipam.graphql.types')]] diff --git a/netbox/virtualization/graphql/types.py b/netbox/virtualization/graphql/types.py index 33d6ce450..9b6e11dc2 100644 --- a/netbox/virtualization/graphql/types.py +++ b/netbox/virtualization/graphql/types.py @@ -33,7 +33,7 @@ class ComponentType(NetBoxObjectType): exclude=('scope_type', 'scope_id', '_location', '_region', '_site', '_site_group'), filters=ClusterFilter ) -class ClusterType(VLANGroupsMixin, NetBoxObjectType): +class ClusterType(ContactsMixin, VLANGroupsMixin, NetBoxObjectType): type: Annotated["ClusterTypeType", strawberry.lazy('virtualization.graphql.types')] | None group: Annotated["ClusterGroupType", strawberry.lazy('virtualization.graphql.types')] | None tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None @@ -55,7 +55,7 @@ class ClusterType(VLANGroupsMixin, NetBoxObjectType): fields='__all__', filters=ClusterGroupFilter ) -class ClusterGroupType(VLANGroupsMixin, OrganizationalObjectType): +class ClusterGroupType(ContactsMixin, VLANGroupsMixin, OrganizationalObjectType): clusters: List[Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')]] diff --git a/netbox/vpn/graphql/types.py b/netbox/vpn/graphql/types.py index 7940bd326..3fec6e791 100644 --- a/netbox/vpn/graphql/types.py +++ b/netbox/vpn/graphql/types.py @@ -27,7 +27,7 @@ __all__ = ( fields='__all__', filters=TunnelGroupFilter ) -class TunnelGroupType(OrganizationalObjectType): +class TunnelGroupType(ContactsMixin, OrganizationalObjectType): tunnels: List[Annotated["TunnelType", strawberry.lazy('vpn.graphql.types')]] @@ -48,7 +48,7 @@ class TunnelTerminationType(CustomFieldsMixin, TagsMixin, ObjectType): fields='__all__', filters=TunnelFilter ) -class TunnelType(NetBoxObjectType): +class TunnelType(ContactsMixin, NetBoxObjectType): group: Annotated["TunnelGroupType", strawberry.lazy('vpn.graphql.types')] | None ipsec_profile: Annotated["IPSecProfileType", strawberry.lazy('vpn.graphql.types')] | None tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None