Merge branch 'main' into 02496-max-page
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

This commit is contained in:
Arthur 2025-10-07 09:12:33 -07:00
commit 4db3d488ad
10 changed files with 77 additions and 43 deletions

View File

@ -215569,24 +215569,26 @@
"IntegerRange": { "IntegerRange": {
"type": "array", "type": "array",
"items": { "items": {
"type": "array", "type": "integer"
"items": { },
"type": "integer" "minItems": 2,
}, "maxItems": 2,
"minItems": 2, "example": [
"maxItems": 2 10,
} 20
]
}, },
"IntegerRangeRequest": { "IntegerRangeRequest": {
"type": "array", "type": "array",
"items": { "items": {
"type": "array", "type": "integer"
"items": { },
"type": "integer" "minItems": 2,
}, "maxItems": 2,
"minItems": 2, "example": [
"maxItems": 2 10,
} 20
]
}, },
"Interface": { "Interface": {
"type": "object", "type": "object",
@ -228986,7 +228988,6 @@
}, },
"key": { "key": {
"type": "string", "type": "string",
"writeOnly": true,
"maxLength": 40, "maxLength": 40,
"minLength": 40 "minLength": 40
}, },
@ -245221,6 +245222,11 @@
"format": "date-time", "format": "date-time",
"nullable": true "nullable": true
}, },
"key": {
"type": "string",
"maxLength": 40,
"minLength": 40
},
"write_enabled": { "write_enabled": {
"type": "boolean", "type": "boolean",
"description": "Permit create/update/delete operations using this key" "description": "Permit create/update/delete operations using this key"
@ -245367,7 +245373,6 @@
}, },
"key": { "key": {
"type": "string", "type": "string",
"writeOnly": true,
"maxLength": 40, "maxLength": 40,
"minLength": 40 "minLength": 40
}, },

View File

@ -1,6 +1,6 @@
# Filters & Filter Sets # 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 ## FilterSet Classes

View File

@ -1,5 +1,7 @@
from django.apps import AppConfig from django.apps import AppConfig
from netbox import denormalized
class CircuitsConfig(AppConfig): class CircuitsConfig(AppConfig):
name = "circuits" name = "circuits"
@ -8,6 +10,16 @@ class CircuitsConfig(AppConfig):
def ready(self): def ready(self):
from netbox.models.features import register_models from netbox.models.features import register_models
from . import signals, search # noqa: F401 from . import signals, search # noqa: F401
from .models import CircuitTermination
# Register models # Register models
register_models(*self.get_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): class FixIntegerRangeSerializerSchema(OpenApiSerializerExtension):
target_class = 'netbox.api.fields.IntegerRangeSerializer' target_class = 'netbox.api.fields.IntegerRangeSerializer'
match_subclasses = True
def map_serializer(self, auto_schema: 'AutoSchema', direction: Direction) -> _SchemaType: def map_serializer(self, auto_schema: 'AutoSchema', direction: Direction) -> _SchemaType:
# One range = two integers; many=True will wrap this in an outer array
return { return {
'type': 'array', 'type': 'array',
'items': { 'items': {
'type': 'array', 'type': 'integer',
'items': {
'type': 'integer',
},
'minItems': 2,
'maxItems': 2,
}, },
'minItems': 2,
'maxItems': 2,
'example': [10, 20],
} }

View File

@ -74,7 +74,7 @@ class BaseIPAddressFamilyType:
filters=ASNFilter, filters=ASNFilter,
pagination=True pagination=True
) )
class ASNType(NetBoxObjectType): class ASNType(NetBoxObjectType, ContactsMixin):
asn: BigInt asn: BigInt
rir: Annotated["RIRType", strawberry.lazy('ipam.graphql.types')] | None rir: Annotated["RIRType", strawberry.lazy('ipam.graphql.types')] | None
tenant: Annotated["TenantType", strawberry.lazy('tenancy.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: if type(data[0]) is not int or type(data[1]) is not int:
raise ValidationError(_("Range boundaries must be defined as integers.")) 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): def to_representation(self, instance):
return instance.lower, instance.upper - 1 return instance.lower, instance.upper - 1

View File

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

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -12755,7 +12755,7 @@ msgstr ""
#: netbox/templates/extras/configtemplate.html:77 #: netbox/templates/extras/configtemplate.html:77
#: netbox/templates/extras/eventrule.html:66 #: netbox/templates/extras/eventrule.html:66
#: netbox/templates/extras/exporttemplate.html:60 #: 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:65
#: netbox/templates/extras/webhook.html:75 #: netbox/templates/extras/webhook.html:75
#: netbox/templates/inc/panel_table.html:13 #: netbox/templates/inc/panel_table.html:13

View File

@ -137,8 +137,17 @@ def check_ranges_overlap(ranges):
def ranges_to_string(ranges): def ranges_to_string(ranges):
""" """
Generate a human-friendly string from a set of ranges. Intended for use with ArrayField. For example: Converts a list of ranges into a string representation.
[[1, 100)], [200, 300)] => "1-99,200-299"
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: if not ranges:
return '' return ''
@ -146,15 +155,22 @@ def ranges_to_string(ranges):
for r in ranges: for r in ranges:
lower = r.lower if r.lower_inc else r.lower + 1 lower = r.lower if r.lower_inc else r.lower + 1
upper = r.upper if r.upper_inc else r.upper - 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) return ','.join(output)
def string_to_ranges(value): 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. Converts a string representation of numeric ranges into a list of NumericRange objects.
For example:
"1-99,200-299" => [NumericRange(1, 100), NumericRange(200, 300)] 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: if not value:
return None return None
@ -172,5 +188,5 @@ def string_to_ranges(value):
upper = dash_range[1] upper = dash_range[1]
else: else:
return None return None
values.append(NumericRange(int(lower), int(upper), bounds='[]')) values.append(NumericRange(int(lower), int(upper) + 1, bounds='[)'))
return values return values

View File

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