2
0
Эх сурвалжийг харах

Merge branch 'main' into 02496-max-page

Arthur 4 сар өмнө
parent
commit
4db3d488ad

+ 21 - 16
contrib/openapi.json

@@ -215569,24 +215569,26 @@
             "IntegerRange": {
             "IntegerRange": {
                 "type": "array",
                 "type": "array",
                 "items": {
                 "items": {
-                    "type": "array",
-                    "items": {
-                        "type": "integer"
-                    },
-                    "minItems": 2,
-                    "maxItems": 2
-                }
+                    "type": "integer"
+                },
+                "minItems": 2,
+                "maxItems": 2,
+                "example": [
+                    10,
+                    20
+                ]
             },
             },
             "IntegerRangeRequest": {
             "IntegerRangeRequest": {
                 "type": "array",
                 "type": "array",
                 "items": {
                 "items": {
-                    "type": "array",
-                    "items": {
-                        "type": "integer"
-                    },
-                    "minItems": 2,
-                    "maxItems": 2
-                }
+                    "type": "integer"
+                },
+                "minItems": 2,
+                "maxItems": 2,
+                "example": [
+                    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
                     },
                     },

+ 1 - 1
docs/plugins/development/filtersets.md

@@ -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
 
 

+ 12 - 0
netbox/circuits/apps.py

@@ -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',
+        })

+ 6 - 6
netbox/core/api/schema.py

@@ -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',
-                'items': {
-                    'type': 'integer',
-                },
-                'minItems': 2,
-                'maxItems': 2,
+                'type': 'integer',
             },
             },
+            'minItems': 2,
+            'maxItems': 2,
+            'example': [10, 20],
         }
         }
 
 
 
 

+ 1 - 1
netbox/ipam/graphql/types.py

@@ -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

+ 1 - 1
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:
         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

+ 4 - 3
netbox/templates/extras/htmx/script_result.html

@@ -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"
-        ></div>
+          hx-trigger="load" hx-select=".htmx-container" hx-swap="outerHTML">
+        </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 %}

+ 2 - 2
netbox/translations/en/LC_MESSAGES/django.po

@@ -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

+ 23 - 7
netbox/utilities/data.py

@@ -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:
-        [[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:
     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.
-    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:
     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

+ 6 - 6
netbox/utilities/tests/test_data.py

@@ -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(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(
         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(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
             ]
             ]
         )
         )