diff --git a/contrib/openapi.json b/contrib/openapi.json index 2e0cbaa07..33b5092af 100644 --- a/contrib/openapi.json +++ b/contrib/openapi.json @@ -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 }, @@ -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 }, diff --git a/docs/plugins/development/filtersets.md b/docs/plugins/development/filtersets.md index e19b3a733..a4b9e1375 100644 --- a/docs/plugins/development/filtersets.md +++ b/docs/plugins/development/filtersets.md @@ -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 diff --git a/netbox/circuits/apps.py b/netbox/circuits/apps.py index 4d5f177e2..7b29a1509 100644 --- a/netbox/circuits/apps.py +++ b/netbox/circuits/apps.py @@ -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', + }) diff --git a/netbox/core/api/schema.py b/netbox/core/api/schema.py index 0c59da5a1..c61675cf3 100644 --- a/netbox/core/api/schema.py +++ b/netbox/core/api/schema.py @@ -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], } diff --git a/netbox/ipam/graphql/types.py b/netbox/ipam/graphql/types.py index e8f98eabe..a07316fe4 100644 --- a/netbox/ipam/graphql/types.py +++ b/netbox/ipam/graphql/types.py @@ -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 diff --git a/netbox/netbox/api/fields.py b/netbox/netbox/api/fields.py index db5ec184d..7dfd7d7eb 100644 --- a/netbox/netbox/api/fields.py +++ b/netbox/netbox/api/fields.py @@ -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 diff --git a/netbox/templates/extras/htmx/script_result.html b/netbox/templates/extras/htmx/script_result.html index ac5d87ef2..3844c7857 100644 --- a/netbox/templates/extras/htmx/script_result.html +++ b/netbox/templates/extras/htmx/script_result.html @@ -44,8 +44,8 @@
+ hx-trigger="load" hx-select=".htmx-container" hx-swap="outerHTML"> + {% endif %} @@ -60,11 +60,12 @@ {% trans "Download" %} + {% copy_content "job_data_output" %} {% endif %} {% if job.data.output %} -
{{ job.data.output }}
+
{{ job.data.output }}
{% else %}
{% trans "None" %}
{% endif %} diff --git a/netbox/translations/en/LC_MESSAGES/django.po b/netbox/translations/en/LC_MESSAGES/django.po index 265540c60..563110ae4 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-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 \n" "Language-Team: LANGUAGE \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 diff --git a/netbox/utilities/data.py b/netbox/utilities/data.py index 7b50d26b8..1e7746197 100644 --- a/netbox/utilities/data.py +++ b/netbox/utilities/data.py @@ -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 diff --git a/netbox/utilities/tests/test_data.py b/netbox/utilities/tests/test_data.py index 7b313baf7..5d211c7bd 100644 --- a/netbox/utilities/tests/test_data.py +++ b/netbox/utilities/tests/test_data.py @@ -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 ] )