Compare commits

..

13 Commits

Author SHA1 Message Date
Arthur
4db3d488ad Merge branch 'main' into 02496-max-page 2025-10-07 09:12:33 -07:00
Martin Hauser
b7cae04572 fix(api): Update NumericRange handling to use half-open intervals (#20478) 2025-10-07 09:01:29 -07:00
Martin Hauser
51528ae429 fix(utilities): Enhance ranges_to_string for improved clarity (#20479) 2025-10-07 08:47:01 -07:00
Jeremy Stretch
d5e8480367 Update OpenAPI schema (#20519) 2025-10-07 08:22:24 -07:00
Matthew Papaleo
05e26b82c1 Fixes #20507 Contacts returned for ASN via graphql API 2025-10-07 09:08:04 -04:00
github-actions
faa89a53ff Update source translation strings 2025-10-07 05:02:29 +00:00
Dmitry Smirnov
d18bbe48c1 add tag copy_content and id 'job_data_output' 2025-10-06 15:17:39 -04:00
Martin Hauser
99e367cbaf docs(api): Correct IntegerRangeSerializer schema definition
Adjusts the schema mapping for `IntegerRangeSerializer` by setting
`match_subclasses` to `True` and refining the array definition. Adds
an example field for clarity in generated OpenAPI documentation.

Fixes #20494
2025-10-06 15:09:57 -04:00
Daniel Sheppard
f5ed095738 Fixes: #21040 - Registered denormalized fields (#20503) 2025-10-06 09:12:27 -05:00
Johannes Erwerle
b70f1211ab Fixed wrong link in plugin filtersets documentation 2025-10-06 10:03:47 -04:00
Arthur
10e8e7b071 20496 fix test 2025-10-03 14:54:08 -07:00
Arthur
c770e6b45d 20496 fix max_page_size for REST API 2025-10-03 14:22:55 -07:00
Jason Novinger
c094699dc0 Fixes #20484: Configure CodeQL to exclude URL redirect false positives 2025-10-03 08:48:02 -04:00
14 changed files with 104 additions and 63 deletions

View File

@@ -1,3 +1,11 @@
paths-ignore:
# Ignore compiled JS
- netbox/project-static/dist
query-filters:
# Exclude py/url-redirection: NetBox uses safe_for_redirect() wrapper function
# which validates all redirects via Django's url_has_allowed_host_and_scheme().
# CodeQL's taint tracking doesn't recognize wrapper functions without custom
# query configuration. See #20484.
- exclude:
id: py/url-redirection

View File

@@ -214760,7 +214760,7 @@
},
"mark_utilized": {
"type": "boolean",
"description": "Report space as 100% utilized"
"description": "Report space as fully utilized"
}
},
"required": [
@@ -214869,7 +214869,7 @@
},
"mark_utilized": {
"type": "boolean",
"description": "Report space as 100% utilized"
"description": "Report space as fully utilized"
}
},
"required": [
@@ -215569,24 +215569,26 @@
"IntegerRange": {
"type": "array",
"items": {
"type": "array",
"items": {
"type": "integer"
},
"minItems": 2,
"maxItems": 2
}
"type": "integer"
},
"minItems": 2,
"maxItems": 2,
"example": [
10,
20
]
},
"IntegerRangeRequest": {
"type": "array",
"items": {
"type": "array",
"items": {
"type": "integer"
},
"minItems": 2,
"maxItems": 2
}
"type": "integer"
},
"minItems": 2,
"maxItems": 2,
"example": [
10,
20
]
},
"Interface": {
"type": "object",
@@ -228986,7 +228988,6 @@
},
"key": {
"type": "string",
"writeOnly": true,
"maxLength": 40,
"minLength": 40
},
@@ -231880,7 +231881,7 @@
},
"mark_utilized": {
"type": "boolean",
"description": "Report space as 100% utilized"
"description": "Report space as fully utilized"
}
}
},
@@ -245221,6 +245222,11 @@
"format": "date-time",
"nullable": true
},
"key": {
"type": "string",
"maxLength": 40,
"minLength": 40
},
"write_enabled": {
"type": "boolean",
"description": "Permit create/update/delete operations using this key"
@@ -245367,7 +245373,6 @@
},
"key": {
"type": "string",
"writeOnly": true,
"maxLength": 40,
"minLength": 40
},
@@ -252203,7 +252208,7 @@
},
"mark_utilized": {
"type": "boolean",
"description": "Report space as 100% utilized"
"description": "Report space as fully utilized"
}
},
"required": [

View File

@@ -1,6 +1,6 @@
# Filters & Filter Sets
Filter sets define the mechanisms available for filtering or searching through a set of objects in NetBox. For instance, sites can be filtered by their parent region or group, status, facility ID, and so on. The same filter set is used consistently for a model whether the request is made via the UI or REST API. (Note that the GraphQL API uses a separate filter class.) NetBox employs the [django-filters2](https://django-tables2.readthedocs.io/en/latest/) library to define filter sets.
Filter sets define the mechanisms available for filtering or searching through a set of objects in NetBox. For instance, sites can be filtered by their parent region or group, status, facility ID, and so on. The same filter set is used consistently for a model whether the request is made via the UI or REST API. (Note that the GraphQL API uses a separate filter class.) NetBox employs the [django-filter](https://django-filter.readthedocs.io/en/stable/) library to define filter sets.
## FilterSet Classes

View File

@@ -1,5 +1,7 @@
from django.apps import AppConfig
from netbox import denormalized
class CircuitsConfig(AppConfig):
name = "circuits"
@@ -8,6 +10,16 @@ class CircuitsConfig(AppConfig):
def ready(self):
from netbox.models.features import register_models
from . import signals, search # noqa: F401
from .models import CircuitTermination
# Register models
register_models(*self.get_models())
denormalized.register(CircuitTermination, '_site', {
'_region': 'region',
'_site_group': 'group',
})
denormalized.register(CircuitTermination, '_location', {
'_site': 'site',
})

View File

@@ -282,18 +282,18 @@ class FixSerializedPKRelatedField(OpenApiSerializerFieldExtension):
class FixIntegerRangeSerializerSchema(OpenApiSerializerExtension):
target_class = 'netbox.api.fields.IntegerRangeSerializer'
match_subclasses = True
def map_serializer(self, auto_schema: 'AutoSchema', direction: Direction) -> _SchemaType:
# One range = two integers; many=True will wrap this in an outer array
return {
'type': 'array',
'items': {
'type': 'array',
'items': {
'type': 'integer',
},
'minItems': 2,
'maxItems': 2,
'type': 'integer',
},
'minItems': 2,
'maxItems': 2,
'example': [10, 20],
}

View File

@@ -618,12 +618,6 @@ class BaseInterface(models.Model):
null=True,
verbose_name=_('primary MAC address')
)
mac_addresses = GenericRelation(
to='dcim.MACAddress',
content_type_field='assigned_object_type',
object_id_field='assigned_object_id',
related_query_name='interface',
)
class Meta:
abstract = True

View File

@@ -74,7 +74,7 @@ class BaseIPAddressFamilyType:
filters=ASNFilter,
pagination=True
)
class ASNType(NetBoxObjectType):
class ASNType(NetBoxObjectType, ContactsMixin):
asn: BigInt
rir: Annotated["RIRType", strawberry.lazy('ipam.graphql.types')] | None
tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None

View File

@@ -169,7 +169,7 @@ class IntegerRangeSerializer(serializers.Serializer):
if type(data[0]) is not int or type(data[1]) is not int:
raise ValidationError(_("Range boundaries must be defined as integers."))
return NumericRange(data[0], data[1], bounds='[]')
return NumericRange(data[0], data[1] + 1, bounds='[)')
def to_representation(self, instance):
return instance.lower, instance.upper - 1

View File

@@ -44,22 +44,28 @@ class OptionalLimitOffsetPagination(LimitOffsetPagination):
return list(queryset[self.offset:])
def get_limit(self, request):
max_limit = self.default_limit
MAX_PAGE_SIZE = get_config().MAX_PAGE_SIZE
if MAX_PAGE_SIZE:
max_limit = min(max_limit, MAX_PAGE_SIZE)
if self.limit_query_param:
MAX_PAGE_SIZE = get_config().MAX_PAGE_SIZE
if MAX_PAGE_SIZE:
MAX_PAGE_SIZE = max(MAX_PAGE_SIZE, self.default_limit)
try:
limit = int(request.query_params[self.limit_query_param])
if limit < 0:
raise ValueError()
# Enforce maximum page size, if defined
if MAX_PAGE_SIZE:
return MAX_PAGE_SIZE if limit == 0 else min(limit, MAX_PAGE_SIZE)
return limit
if limit == 0:
max_limit = MAX_PAGE_SIZE
else:
max_limit = min(MAX_PAGE_SIZE, limit)
else:
max_limit = limit
except (KeyError, ValueError):
pass
return self.default_limit
return max_limit
def get_queryset_count(self, queryset):
return queryset.count()

View File

@@ -44,8 +44,8 @@
<div class="htmx-container table-responsive"
hx-get="{% url 'extras:script_result' job_pk=job.pk %}?embedded=True&log=True&log_threshold={{log_threshold}}"
hx-target="this"
hx-trigger="load" hx-select=".htmx-container" hx-swap="outerHTML"
></div>
hx-trigger="load" hx-select=".htmx-container" hx-swap="outerHTML">
</div>
</div>
</div>
{% endif %}
@@ -60,11 +60,12 @@
<a href="?export=output" class="btn btn-sm btn-primary" role="button">
<i class="mdi mdi-download" aria-hidden="true"></i> {% trans "Download" %}
</a>
{% copy_content "job_data_output" %}
</div>
{% endif %}
</h2>
{% if job.data.output %}
<pre class="card-body font-monospace">{{ job.data.output }}</pre>
<pre class="card-body font-monospace" id="job_data_output">{{ job.data.output }}</pre>
{% else %}
<div class="card-body text-muted">{% trans "None" %}</div>
{% endif %}

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-02 05:01+0000\n"
"POT-Creation-Date: 2025-10-07 05:02+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -12755,7 +12755,7 @@ msgstr ""
#: netbox/templates/extras/configtemplate.html:77
#: netbox/templates/extras/eventrule.html:66
#: netbox/templates/extras/exporttemplate.html:60
#: netbox/templates/extras/htmx/script_result.html:69
#: netbox/templates/extras/htmx/script_result.html:70
#: netbox/templates/extras/webhook.html:65
#: netbox/templates/extras/webhook.html:75
#: netbox/templates/inc/panel_table.html:13

View File

@@ -137,8 +137,17 @@ def check_ranges_overlap(ranges):
def ranges_to_string(ranges):
"""
Generate a human-friendly string from a set of ranges. Intended for use with ArrayField. For example:
[[1, 100)], [200, 300)] => "1-99,200-299"
Converts a list of ranges into a string representation.
This function takes a list of range objects and produces a string
representation of those ranges. Each range is represented as a
hyphen-separated pair of lower and upper bounds, with inclusive or
exclusive bounds adjusted accordingly. If the lower and upper bounds
of a range are the same, only the single value is added to the string.
Intended for use with ArrayField.
Example:
[NumericRange(1, 5), NumericRange(8, 9), NumericRange(10, 12)] => "1-5,8,10-12"
"""
if not ranges:
return ''
@@ -146,15 +155,22 @@ def ranges_to_string(ranges):
for r in ranges:
lower = r.lower if r.lower_inc else r.lower + 1
upper = r.upper if r.upper_inc else r.upper - 1
output.append(f'{lower}-{upper}')
output.append(f"{lower}-{upper}" if lower != upper else str(lower))
return ','.join(output)
def string_to_ranges(value):
"""
Given a string in the format "1-100, 200-300" return an list of NumericRanges. Intended for use with ArrayField.
For example:
"1-99,200-299" => [NumericRange(1, 100), NumericRange(200, 300)]
Converts a string representation of numeric ranges into a list of NumericRange objects.
This function parses a string containing numeric values and ranges separated by commas (e.g.,
"1-5,8,10-12") and converts it into a list of NumericRange objects.
In the case of a single integer, it is treated as a range where the start and end
are equal. The returned ranges are represented as half-open intervals [lower, upper).
Intended for use with ArrayField.
Example:
"1-5,8,10-12" => [NumericRange(1, 6), NumericRange(8, 9), NumericRange(10, 13)]
"""
if not value:
return None
@@ -172,5 +188,5 @@ def string_to_ranges(value):
upper = dash_range[1]
else:
return None
values.append(NumericRange(int(lower), int(upper), bounds='[]'))
values.append(NumericRange(int(lower), int(upper) + 1, bounds='[)'))
return values

View File

@@ -149,14 +149,13 @@ class APIPaginationTestCase(APITestCase):
def test_default_page_size_with_small_max_page_size(self):
response = self.client.get(self.url, format='json', **self.header)
page_size = get_config().MAX_PAGE_SIZE
paginate_count = get_config().PAGINATE_COUNT
self.assertLess(page_size, 100, "Default page size not sufficient for data set")
self.assertHttpStatus(response, status.HTTP_200_OK)
self.assertEqual(response.data['count'], 100)
self.assertTrue(response.data['next'].endswith(f'?limit={paginate_count}&offset={paginate_count}'))
self.assertTrue(response.data['next'].endswith(f'?limit={page_size}&offset={page_size}'))
self.assertIsNone(response.data['previous'])
self.assertEqual(len(response.data['results']), paginate_count)
self.assertEqual(len(response.data['results']), page_size)
def test_custom_page_size(self):
response = self.client.get(f'{self.url}?limit=10', format='json', **self.header)

View File

@@ -61,18 +61,18 @@ class RangeFunctionsTestCase(TestCase):
self.assertEqual(
string_to_ranges('10-19, 30-39, 100-199'),
[
NumericRange(10, 19, bounds='[]'), # 10-19
NumericRange(30, 39, bounds='[]'), # 30-39
NumericRange(100, 199, bounds='[]'), # 100-199
NumericRange(10, 20, bounds='[)'), # 10-20
NumericRange(30, 40, bounds='[)'), # 30-40
NumericRange(100, 200, bounds='[)'), # 100-200
]
)
self.assertEqual(
string_to_ranges('1-2, 5, 10-12'),
[
NumericRange(1, 2, bounds='[]'), # 1-2
NumericRange(5, 5, bounds='[]'), # 5-5
NumericRange(10, 12, bounds='[]'), # 10-12
NumericRange(1, 3, bounds='[)'), # 1-3
NumericRange(5, 6, bounds='[)'), # 5-6
NumericRange(10, 13, bounds='[)'), # 10-13
]
)