Compare commits

...

12 Commits

Author SHA1 Message Date
Martin Hauser
71ad3cd088 Merge 60fce84c96 into 21f4036782 2025-12-11 23:52:11 -06:00
github-actions
21f4036782 Update source translation strings
Some checks are pending
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
2025-12-12 05:03:16 +00:00
bctiemann
ce3738572c Merge pull request #20967 from netbox-community/20966-remove-stick-scroll
Some checks are pending
CI / build (20.x, 3.10) (push) Waiting to run
CI / build (20.x, 3.11) (push) Waiting to run
CI / build (20.x, 3.12) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
Fixes #20966: Fix broken optgroup stickiness in ObjectType multiselect
2025-12-11 19:44:16 -05:00
bctiemann
cbb979934e Merge pull request #20958 from netbox-community/17976-manufacturer-devicetype_count
Fixes #17976: Remove devicetype_count from nested manufacturer to correct OpenAPI schema
2025-12-11 19:42:26 -05:00
bctiemann
642d83a4c6 Merge pull request #20937 from netbox-community/20560-bulk-import-prefix
Fixes #20560: Fix VLAN disambiguation in prefix bulk import
2025-12-11 19:40:59 -05:00
Jason Novinger
a06c12c6b8 Fixes #20966: Fix broken optgroup stickiness in ObjectType multiselect 2025-12-11 08:59:16 -06:00
Martin Hauser
60fce84c96 feat(ipam): Normalize numeric ranges in API output
Adds logic to handle numeric range fields in API responses by
converting them into inclusive `[low, high]` pairs for consistent
behavior. Updates test cases with `vid_ranges` fields to reflect the
changes.

Closes #20491
2025-12-10 21:11:23 +01:00
Jeremy Stretch
59afa0b41d Fix test 2025-12-10 09:01:11 -05:00
Jeremy Stretch
14b246cb8a Fixes #17976: Remove devicetype_count from nested manufacturer to correct OpenAPI schema 2025-12-10 08:23:48 -05:00
github-actions
f0507d00bf Update source translation strings
Some checks failed
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Has been cancelled
2025-12-10 05:02:48 +00:00
Arthur Hanson
77b389f105 Fixes #20873: fix webhooks with image fields (#20955) 2025-12-09 22:06:11 -06:00
Jason Novinger
9ae53fc232 Fixes #20560: Fix VLAN disambiguation in prefix bulk import 2025-12-05 16:39:28 -06:00
11 changed files with 494 additions and 405 deletions

View File

@@ -20,4 +20,4 @@ class ManufacturerSerializer(NetBoxModelSerializer):
'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields',
'created', 'last_updated', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'created', 'last_updated', 'devicetype_count', 'inventoryitem_count', 'platform_count',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'devicetype_count') brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description')

View File

@@ -531,7 +531,7 @@ class RackReservationTest(APIViewTestCases.APIViewTestCase):
class ManufacturerTest(APIViewTestCases.APIViewTestCase): class ManufacturerTest(APIViewTestCases.APIViewTestCase):
model = Manufacturer model = Manufacturer
brief_fields = ['description', 'devicetype_count', 'display', 'id', 'name', 'slug', 'url'] brief_fields = ['description', 'display', 'id', 'name', 'slug', 'url']
create_data = [ create_data = [
{ {
'name': 'Manufacturer 4', 'name': 'Manufacturer 4',

View File

@@ -119,7 +119,9 @@ def process_event_rules(event_rules, object_type, event_type, data, username=Non
if snapshots: if snapshots:
params["snapshots"] = snapshots params["snapshots"] = snapshots
if request: if request:
params["request"] = copy_safe_request(request) # Exclude FILES - webhooks don't need uploaded files,
# which can cause pickle errors with Pillow.
params["request"] = copy_safe_request(request, include_files=False)
# Enqueue the task # Enqueue the task
rq_queue.enqueue( rq_queue.enqueue(

View File

@@ -230,10 +230,6 @@ class PrefixImportForm(ScopedImportForm, NetBoxModelImportForm):
query |= Q(**{ query |= Q(**{
f"site__{self.fields['vlan_site'].to_field_name}": vlan_site f"site__{self.fields['vlan_site'].to_field_name}": vlan_site
}) })
# Don't Forget to include VLANs without a site in the filter
query |= Q(**{
f"site__{self.fields['vlan_site'].to_field_name}__isnull": True
})
if vlan_group: if vlan_group:
query &= Q(**{ query &= Q(**{

View File

@@ -1071,14 +1071,17 @@ class VLANGroupTest(APIViewTestCases.APIViewTestCase):
{ {
'name': 'VLAN Group 4', 'name': 'VLAN Group 4',
'slug': 'vlan-group-4', 'slug': 'vlan-group-4',
'vid_ranges': [[1, 4094]]
}, },
{ {
'name': 'VLAN Group 5', 'name': 'VLAN Group 5',
'slug': 'vlan-group-5', 'slug': 'vlan-group-5',
'vid_ranges': [[1, 4094]]
}, },
{ {
'name': 'VLAN Group 6', 'name': 'VLAN Group 6',
'slug': 'vlan-group-6', 'slug': 'vlan-group-6',
'vid_ranges': [[1, 4094]]
}, },
] ]
bulk_update_data = { bulk_update_data = {

View File

@@ -564,6 +564,82 @@ vlan: 102
self.assertEqual(prefix.vlan.vid, 102) self.assertEqual(prefix.vlan.vid, 102)
self.assertEqual(prefix.scope, site) self.assertEqual(prefix.scope, site)
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_prefix_import_with_vlan_site_multiple_vlans_same_vid(self):
"""
Test import when multiple VLANs exist with the same vid but different sites.
Ref: #20560
"""
site1 = Site.objects.get(name='Site 1')
site2 = Site.objects.get(name='Site 2')
# Create VLANs with the same vid but different sites
vlan1 = VLAN.objects.create(vid=1, name='VLAN1-Site1', site=site1)
VLAN.objects.create(vid=1, name='VLAN1-Site2', site=site2) # Create ambiguity
# Import prefix with vlan_site specified
IMPORT_DATA = f"""
prefix: 10.11.0.0/22
status: active
scope_type: dcim.site
scope_id: {site1.pk}
vlan_site: {site1.name}
vlan: 1
description: LOC02-MGMT
"""
# Add all required permissions to the test user
self.add_permissions('ipam.view_prefix', 'ipam.add_prefix')
form_data = {
'data': IMPORT_DATA,
'format': 'yaml'
}
response = self.client.post(reverse('ipam:prefix_bulk_import'), data=form_data, follow=True)
self.assertHttpStatus(response, 200)
# Verify the prefix was created with the correct VLAN
prefix = Prefix.objects.get(prefix='10.11.0.0/22')
self.assertEqual(prefix.vlan, vlan1)
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_prefix_import_with_vlan_site_and_global_vlan(self):
"""
Test import when a global VLAN (no site) and site-specific VLAN exist with same vid.
When vlan_site is specified, should prefer the site-specific VLAN.
Ref: #20560
"""
site1 = Site.objects.get(name='Site 1')
# Create a global VLAN (no site) and a site-specific VLAN with the same vid
VLAN.objects.create(vid=10, name='VLAN10-Global', site=None) # Create ambiguity
vlan_site = VLAN.objects.create(vid=10, name='VLAN10-Site1', site=site1)
# Import prefix with vlan_site specified
IMPORT_DATA = f"""
prefix: 10.12.0.0/22
status: active
scope_type: dcim.site
scope_id: {site1.pk}
vlan_site: {site1.name}
vlan: 10
description: Test Site-Specific VLAN
"""
# Add all required permissions to the test user
self.add_permissions('ipam.view_prefix', 'ipam.add_prefix')
form_data = {
'data': IMPORT_DATA,
'format': 'yaml'
}
response = self.client.post(reverse('ipam:prefix_bulk_import'), data=form_data, follow=True)
self.assertHttpStatus(response, 200)
# Verify the prefix was created with the site-specific VLAN (not the global one)
prefix = Prefix.objects.get(prefix='10.12.0.0/22')
self.assertEqual(prefix.vlan, vlan_site)
class IPRangeTestCase(ViewTestCases.PrimaryObjectViewTestCase): class IPRangeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = IPRange model = IPRange

File diff suppressed because one or more lines are too long

View File

@@ -36,7 +36,6 @@ form.object-edit {
// Make optgroup labels sticky when scrolling through select elements // Make optgroup labels sticky when scrolling through select elements
select[multiple] { select[multiple] {
optgroup { optgroup {
position: sticky;
top: 0; top: 0;
background-color: var(--bs-body-bg); background-color: var(--bs-body-bg);
font-style: normal; font-style: normal;

File diff suppressed because it is too large Load Diff

View File

@@ -35,27 +35,34 @@ class NetBoxFakeRequest:
# Utility functions # Utility functions
# #
def copy_safe_request(request): def copy_safe_request(request, include_files=True):
""" """
Copy selected attributes from a request object into a new fake request object. This is needed in places where Copy selected attributes from a request object into a new fake request object. This is needed in places where
thread safe pickling of the useful request data is needed. thread safe pickling of the useful request data is needed.
Args:
request: The original request object
include_files: Whether to include request.FILES.
""" """
meta = { meta = {
k: request.META[k] k: request.META[k]
for k in HTTP_REQUEST_META_SAFE_COPY for k in HTTP_REQUEST_META_SAFE_COPY
if k in request.META and isinstance(request.META[k], str) if k in request.META and isinstance(request.META[k], str)
} }
return NetBoxFakeRequest({ data = {
'META': meta, 'META': meta,
'COOKIES': request.COOKIES, 'COOKIES': request.COOKIES,
'POST': request.POST, 'POST': request.POST,
'GET': request.GET, 'GET': request.GET,
'FILES': request.FILES,
'user': request.user, 'user': request.user,
'method': request.method, 'method': request.method,
'path': request.path, 'path': request.path,
'id': getattr(request, 'id', None), # UUID assigned by middleware 'id': getattr(request, 'id', None), # UUID assigned by middleware
}) }
if include_files:
data['FILES'] = request.FILES
return NetBoxFakeRequest(data)
def get_client_ip(request, additional_headers=()): def get_client_ip(request, additional_headers=()):

View File

@@ -141,8 +141,8 @@ class ModelTestCase(TestCase):
elif value and type(field) is GenericForeignKey: elif value and type(field) is GenericForeignKey:
model_dict[key] = value.pk model_dict[key] = value.pk
# Handle API output
elif api: elif api:
# Replace ContentType numeric IDs with <app_label>.<model> # Replace ContentType numeric IDs with <app_label>.<model>
if type(getattr(instance, key)) in (ContentType, ObjectType): if type(getattr(instance, key)) in (ContentType, ObjectType):
object_type = ObjectType.objects.get(pk=value) object_type = ObjectType.objects.get(pk=value)
@@ -152,9 +152,13 @@ class ModelTestCase(TestCase):
elif type(value) is IPNetwork: elif type(value) is IPNetwork:
model_dict[key] = str(value) model_dict[key] = str(value)
else: # Normalize arrays of numeric ranges (e.g. VLAN IDs or port ranges).
field = instance._meta.get_field(key) # DB uses canonical half-open [lo, hi) via NumericRange; API uses inclusive [lo, hi].
# Convert to inclusive pairs for stable API comparisons.
elif type(field) is ArrayField and issubclass(type(field.base_field), RangeField):
model_dict[key] = [[r.lower, r.upper - 1] for r in value]
else:
# Convert ArrayFields to CSV strings # Convert ArrayFields to CSV strings
if type(field) is ArrayField: if type(field) is ArrayField:
if getattr(field.base_field, 'choices', None): if getattr(field.base_field, 'choices', None):