mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-14 06:13:32 +01:00
Compare commits
13 Commits
19720-maca
...
02496-max-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4db3d488ad | ||
|
|
b7cae04572 | ||
|
|
51528ae429 | ||
|
|
d5e8480367 | ||
|
|
05e26b82c1 | ||
|
|
faa89a53ff | ||
|
|
d18bbe48c1 | ||
|
|
99e367cbaf | ||
|
|
f5ed095738 | ||
|
|
b70f1211ab | ||
|
|
10e8e7b071 | ||
|
|
c770e6b45d | ||
|
|
c094699dc0 |
8
.github/codeql/codeql-config.yml
vendored
8
.github/codeql/codeql-config.yml
vendored
@@ -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
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
|
||||
@@ -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],
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user