Sfoglia il codice sorgente

Merge branch 'main' into feature

Jeremy Stretch 6 mesi fa
parent
commit
40dd36812c
68 ha cambiato i file con 3643 aggiunte e 2828 eliminazioni
  1. 1 1
      .github/ISSUE_TEMPLATE/01-feature_request.yaml
  2. 1 1
      .github/ISSUE_TEMPLATE/02-bug_report.yaml
  3. 4 3
      base_requirements.txt
  4. 2 2
      docs/administration/replicating-netbox.md
  5. 0 7
      docs/installation/3-netbox.md
  6. 8 0
      docs/models/extras/configtemplate.md
  7. 8 0
      docs/models/extras/exporttemplate.md
  8. 14 12
      docs/reference/filtering.md
  9. 16 0
      docs/release-notes/version-4.3.md
  10. 8 11
      netbox/core/api/views.py
  11. 11 0
      netbox/core/jobs.py
  12. 1 6
      netbox/core/views.py
  13. 63 3
      netbox/dcim/forms/bulk_import.py
  14. 30 1
      netbox/dcim/svg/racks.py
  15. 23 11
      netbox/dcim/tests/test_views.py
  16. 3 1
      netbox/extras/api/views.py
  17. 6 0
      netbox/extras/constants.py
  18. 15 4
      netbox/extras/models/mixins.py
  19. 3 0
      netbox/extras/models/tags.py
  20. 4 0
      netbox/netbox/api/exceptions.py
  21. 7 0
      netbox/netbox/api/pagination.py
  22. 42 0
      netbox/netbox/tests/test_api.py
  23. 0 0
      netbox/project-static/dist/netbox.js
  24. 0 0
      netbox/project-static/dist/netbox.js.map
  25. 2 2
      netbox/project-static/src/racks.ts
  26. 262 252
      netbox/project-static/yarn.lock
  27. 2 2
      netbox/release.yaml
  28. 3 3
      netbox/templates/base/layout.html
  29. 1 1
      netbox/templates/dcim/inc/rack_elevation.html
  30. 7 2
      netbox/tenancy/forms/bulk_edit.py
  31. 1 0
      netbox/tenancy/tests/test_views.py
  32. BIN
      netbox/translations/cs/LC_MESSAGES/django.mo
  33. 208 172
      netbox/translations/cs/LC_MESSAGES/django.po
  34. BIN
      netbox/translations/da/LC_MESSAGES/django.mo
  35. 206 170
      netbox/translations/da/LC_MESSAGES/django.po
  36. BIN
      netbox/translations/de/LC_MESSAGES/django.mo
  37. 208 172
      netbox/translations/de/LC_MESSAGES/django.po
  38. 126 104
      netbox/translations/en/LC_MESSAGES/django.po
  39. BIN
      netbox/translations/es/LC_MESSAGES/django.mo
  40. 208 170
      netbox/translations/es/LC_MESSAGES/django.po
  41. BIN
      netbox/translations/fr/LC_MESSAGES/django.mo
  42. 208 170
      netbox/translations/fr/LC_MESSAGES/django.po
  43. BIN
      netbox/translations/it/LC_MESSAGES/django.mo
  44. 208 170
      netbox/translations/it/LC_MESSAGES/django.po
  45. BIN
      netbox/translations/ja/LC_MESSAGES/django.mo
  46. 204 170
      netbox/translations/ja/LC_MESSAGES/django.po
  47. BIN
      netbox/translations/nl/LC_MESSAGES/django.mo
  48. 208 170
      netbox/translations/nl/LC_MESSAGES/django.po
  49. BIN
      netbox/translations/pl/LC_MESSAGES/django.mo
  50. 208 172
      netbox/translations/pl/LC_MESSAGES/django.po
  51. BIN
      netbox/translations/pt/LC_MESSAGES/django.mo
  52. 208 170
      netbox/translations/pt/LC_MESSAGES/django.po
  53. BIN
      netbox/translations/ru/LC_MESSAGES/django.mo
  54. 208 170
      netbox/translations/ru/LC_MESSAGES/django.po
  55. BIN
      netbox/translations/tr/LC_MESSAGES/django.mo
  56. 210 174
      netbox/translations/tr/LC_MESSAGES/django.po
  57. BIN
      netbox/translations/uk/LC_MESSAGES/django.mo
  58. 208 170
      netbox/translations/uk/LC_MESSAGES/django.po
  59. BIN
      netbox/translations/zh/LC_MESSAGES/django.mo
  60. 205 170
      netbox/translations/zh/LC_MESSAGES/django.po
  61. 17 0
      netbox/users/migrations/0010_add_token_meta_ordering.py
  62. 1 1
      netbox/users/migrations/0011_concrete_objecttype.py
  63. 1 0
      netbox/users/models/tokens.py
  64. 2 0
      netbox/utilities/constants.py
  65. 3 0
      netbox/utilities/query.py
  66. 32 0
      netbox/utilities/tests/test_filters.py
  67. 1 1
      pyproject.toml
  68. 7 7
      requirements.txt

+ 1 - 1
.github/ISSUE_TEMPLATE/01-feature_request.yaml

@@ -15,7 +15,7 @@ body:
     attributes:
     attributes:
       label: NetBox version
       label: NetBox version
       description: What version of NetBox are you currently running?
       description: What version of NetBox are you currently running?
-      placeholder: v4.3.4
+      placeholder: v4.3.5
     validations:
     validations:
       required: true
       required: true
   - type: dropdown
   - type: dropdown

+ 1 - 1
.github/ISSUE_TEMPLATE/02-bug_report.yaml

@@ -27,7 +27,7 @@ body:
     attributes:
     attributes:
       label: NetBox Version
       label: NetBox Version
       description: What version of NetBox are you currently running?
       description: What version of NetBox are you currently running?
-      placeholder: v4.3.4
+      placeholder: v4.3.5
     validations:
     validations:
       required: true
       required: true
   - type: dropdown
   - type: dropdown

+ 4 - 3
base_requirements.txt

@@ -8,7 +8,9 @@ django-cors-headers
 
 
 # Runtime UI tool for debugging Django
 # Runtime UI tool for debugging Django
 # https://github.com/jazzband/django-debug-toolbar/blob/main/docs/changes.rst
 # https://github.com/jazzband/django-debug-toolbar/blob/main/docs/changes.rst
-django-debug-toolbar
+# django-debug-toolbar v6.0.0 raises "Attribute Error at /: 'function' object has no attribute 'set'" 
+# see https://github.com/netbox-community/netbox/issues/19974
+django-debug-toolbar==5.2.0
 
 
 # Library for writing reusable URL query filters
 # Library for writing reusable URL query filters
 # https://github.com/carltongibson/django-filter/blob/main/CHANGES.rst
 # https://github.com/carltongibson/django-filter/blob/main/CHANGES.rst
@@ -145,8 +147,7 @@ strawberry-graphql
 
 
 # Strawberry GraphQL Django extension
 # Strawberry GraphQL Django extension
 # https://github.com/strawberry-graphql/strawberry-django/releases
 # https://github.com/strawberry-graphql/strawberry-django/releases
-# See #19771
-strawberry-graphql-django==0.60.0
+strawberry-graphql-django
 
 
 # SVG image rendering (used for rack elevations)
 # SVG image rendering (used for rack elevations)
 # https://github.com/mozman/svgwrite/blob/master/NEWS.rst
 # https://github.com/mozman/svgwrite/blob/master/NEWS.rst

+ 2 - 2
docs/administration/replicating-netbox.md

@@ -18,10 +18,10 @@ pg_dump --username netbox --password --host localhost netbox > netbox.sql
 !!! note
 !!! note
     You may need to change the username, host, and/or database in the command above to match your installation.
     You may need to change the username, host, and/or database in the command above to match your installation.
 
 
-When replicating a production database for development purposes, you may find it convenient to exclude changelog data, which can easily account for the bulk of a database's size. To do this, exclude the `extras_objectchange` table data from the export. The table will still be included in the output file, but will not be populated with any data.
+When replicating a production database for development purposes, you may find it convenient to exclude changelog data, which can easily account for the bulk of a database's size. To do this, exclude the `core_objectchange` table data from the export. The table will still be included in the output file, but will not be populated with any data.
 
 
 ```no-highlight
 ```no-highlight
-pg_dump ... --exclude-table-data=extras_objectchange netbox > netbox.sql
+pg_dump ... --exclude-table-data=core_objectchange netbox > netbox.sql
 ```
 ```
 
 
 ### Load an Exported Database
 ### Load an Exported Database

+ 0 - 7
docs/installation/3-netbox.md

@@ -290,13 +290,6 @@ Quit the server with CONTROL-C.
 
 
 Next, connect to the name or IP of the server (as defined in `ALLOWED_HOSTS`) on port 8000; for example, <http://127.0.0.1:8000/>. You should be greeted with the NetBox home page. Try logging in using the username and password specified when creating a superuser.
 Next, connect to the name or IP of the server (as defined in `ALLOWED_HOSTS`) on port 8000; for example, <http://127.0.0.1:8000/>. You should be greeted with the NetBox home page. Try logging in using the username and password specified when creating a superuser.
 
 
-!!! note
-    By default RHEL based distros will likely block your testing attempts with firewalld. The development server port can be opened with `firewall-cmd` (add `--permanent` if you want the rule to survive server restarts):
-
-    ```no-highlight
-    firewall-cmd --zone=public --add-port=8000/tcp
-    ```
-
 !!! danger "Not for production use"
 !!! danger "Not for production use"
     The development server is for development and testing purposes only. It is neither performant nor secure enough for production use. **Do not use it in production.**
     The development server is for development and testing purposes only. It is neither performant nor secure enough for production use. **Do not use it in production.**
 
 

+ 8 - 0
docs/models/extras/configtemplate.md

@@ -24,6 +24,14 @@ Jinja2 template code, if being defined locally rather than replicated from a dat
 
 
 A dictionary of any additional parameters to pass when instantiating the [Jinja2 environment](https://jinja.palletsprojects.com/en/3.1.x/api/#jinja2.Environment). Jinja2 supports various optional parameters which can be used to modify its default behavior.
 A dictionary of any additional parameters to pass when instantiating the [Jinja2 environment](https://jinja.palletsprojects.com/en/3.1.x/api/#jinja2.Environment). Jinja2 supports various optional parameters which can be used to modify its default behavior.
 
 
+The `undefined` and `finalize` Jinja environment parameters, which must reference a Python class or function, can define a dotted path to the desired resource. For example:
+
+```json
+{
+    "undefined": "jinja2.StrictUndefined"
+}
+```
+
 ### MIME Type
 ### MIME Type
 
 
 !!! info "This field was introduced in NetBox v4.3."
 !!! info "This field was introduced in NetBox v4.3."

+ 8 - 0
docs/models/extras/exporttemplate.md

@@ -26,6 +26,14 @@ Jinja2 template code for rendering the exported data.
 
 
 A dictionary of any additional parameters to pass when instantiating the [Jinja2 environment](https://jinja.palletsprojects.com/en/3.1.x/api/#jinja2.Environment). Jinja2 supports various optional parameters which can be used to modify its default behavior.
 A dictionary of any additional parameters to pass when instantiating the [Jinja2 environment](https://jinja.palletsprojects.com/en/3.1.x/api/#jinja2.Environment). Jinja2 supports various optional parameters which can be used to modify its default behavior.
 
 
+The `undefined` and `finalize` Jinja environment parameters, which must reference a Python class or function, can define a dotted path to the desired resource. For example:
+
+```json
+{
+    "undefined": "jinja2.StrictUndefined"
+}
+```
+
 ### MIME Type
 ### MIME Type
 
 
 The MIME type to indicate in the response when rendering the export template (optional). Defaults to `text/plain`.
 The MIME type to indicate in the response when rendering the export template (optional). Defaults to `text/plain`.

+ 14 - 12
docs/reference/filtering.md

@@ -80,18 +80,20 @@ GET /api/ipam/vlans/?vid__gt=900
 
 
 String based (char) fields (Name, Address, etc) support these lookup expressions:
 String based (char) fields (Name, Address, etc) support these lookup expressions:
 
 
-| Filter  | Description                            |
-|---------|----------------------------------------|
-| `n`     | Not equal to                           |
-| `ic`    | Contains (case-insensitive)            |
-| `nic`   | Does not contain (case-insensitive)    |
-| `isw`   | Starts with (case-insensitive)         |
-| `nisw`  | Does not start with (case-insensitive) |
-| `iew`   | Ends with (case-insensitive)           |
-| `niew`  | Does not end with (case-insensitive)   |
-| `ie`    | Exact match (case-insensitive)         |
-| `nie`   | Inverse exact match (case-insensitive) |
-| `empty` | Is empty/null (boolean)                |
+| Filter   | Description                            |
+|----------|----------------------------------------|
+| `n`      | Not equal to                           |
+| `ic`     | Contains (case-insensitive)            |
+| `nic`    | Does not contain (case-insensitive)    |
+| `isw`    | Starts with (case-insensitive)         |
+| `nisw`   | Does not start with (case-insensitive) |
+| `iew`    | Ends with (case-insensitive)           |
+| `niew`   | Does not end with (case-insensitive)   |
+| `ie`     | Exact match (case-insensitive)         |
+| `nie`    | Inverse exact match (case-insensitive) |
+| `empty`  | Is empty/null (boolean)                |
+| `regex`  | Regexp matching                        |
+| `iregex` | Regexp matching (case-insensitive)     |
 
 
 Here is an example of a lookup expression on a string field that will return all devices with `switch` in the name:
 Here is an example of a lookup expression on a string field that will return all devices with `switch` in the name:
 
 

+ 16 - 0
docs/release-notes/version-4.3.md

@@ -1,5 +1,21 @@
 # NetBox v4.3
 # NetBox v4.3
 
 
+## v4.3.5 (2025-07-29)
+
+### Enhancements
+* [#18797](https://github.com/netbox-community/netbox/issues/18797) - Added jinja2.StrictUndefined option for config template rendering to catch undefined variables
+* [#18936](https://github.com/netbox-community/netbox/issues/18936) - Cable imports now accept color names (e.g. "red", "blue") in addition to hex color codes
+* [#19840](https://github.com/netbox-community/netbox/issues/19840) - Cable imports now support specifying site information for better organization
+* [#19902](https://github.com/netbox-community/netbox/issues/19902) - Device names in rack elevation SVG exports are automatically truncated to prevent overflow beyond rack unit boundaries
+* [#19903](https://github.com/netbox-community/netbox/issues/19903) - String field filters now support `regex` and `iregex` lookups for advanced pattern matching
+* [#19910](https://github.com/netbox-community/netbox/issues/19910) - Internet-dependent links are no longer visible when running in air-gapped environments
+
+### Bug Fixes
+* [#18900](https://github.com/netbox-community/netbox/issues/18900) - REST API paginator now raises proper exceptions when attempting to paginate unordered querysets
+* [#19916](https://github.com/netbox-community/netbox/issues/19916) - Rack elevation image/label dropdown functionality restored
+* [#19934](https://github.com/netbox-community/netbox/issues/19934) - Added missing description field to tenant bulk edit form
+* [#19956](https://github.com/netbox-community/netbox/issues/19956) - Prevent duplicate deletion records in changelog from cascading deletions
+
 ## v4.3.4 (2025-07-15)
 ## v4.3.4 (2025-07-15)
 
 
 ### Enhancements
 ### Enhancements

+ 8 - 11
netbox/core/api/views.py

@@ -1,30 +1,29 @@
 from django.http import Http404, HttpResponse
 from django.http import Http404, HttpResponse
 from django.shortcuts import get_object_or_404
 from django.shortcuts import get_object_or_404
 from django.utils.translation import gettext_lazy as _
 from django.utils.translation import gettext_lazy as _
+from django_rq.queues import get_redis_connection
+from django_rq.settings import QUEUES_LIST
+from django_rq.utils import get_statistics
 from drf_spectacular.types import OpenApiTypes
 from drf_spectacular.types import OpenApiTypes
 from drf_spectacular.utils import extend_schema
 from drf_spectacular.utils import extend_schema
+from rest_framework import viewsets
 from rest_framework.decorators import action
 from rest_framework.decorators import action
 from rest_framework.exceptions import PermissionDenied
 from rest_framework.exceptions import PermissionDenied
+from rest_framework.permissions import IsAdminUser
 from rest_framework.response import Response
 from rest_framework.response import Response
 from rest_framework.routers import APIRootView
 from rest_framework.routers import APIRootView
 from rest_framework.viewsets import ReadOnlyModelViewSet
 from rest_framework.viewsets import ReadOnlyModelViewSet
+from rq.job import Job as RQ_Job
+from rq.worker import Worker
 
 
 from core import filtersets
 from core import filtersets
-from core.choices import DataSourceStatusChoices
 from core.jobs import SyncDataSourceJob
 from core.jobs import SyncDataSourceJob
 from core.models import *
 from core.models import *
 from core.utils import delete_rq_job, enqueue_rq_job, get_rq_jobs, requeue_rq_job, stop_rq_job
 from core.utils import delete_rq_job, enqueue_rq_job, get_rq_jobs, requeue_rq_job, stop_rq_job
-from django_rq.queues import get_redis_connection
-from django_rq.utils import get_statistics
-from django_rq.settings import QUEUES_LIST
 from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
 from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
 from netbox.api.metadata import ContentTypeMetadata
 from netbox.api.metadata import ContentTypeMetadata
 from netbox.api.pagination import LimitOffsetListPagination
 from netbox.api.pagination import LimitOffsetListPagination
 from netbox.api.viewsets import NetBoxModelViewSet, NetBoxReadOnlyModelViewSet
 from netbox.api.viewsets import NetBoxModelViewSet, NetBoxReadOnlyModelViewSet
-from rest_framework import viewsets
-from rest_framework.permissions import IsAdminUser
-from rq.job import Job as RQ_Job
-from rq.worker import Worker
 from . import serializers
 from . import serializers
 
 
 
 
@@ -51,10 +50,8 @@ class DataSourceViewSet(NetBoxModelViewSet):
         if not request.user.has_perm('core.sync_datasource', obj=datasource):
         if not request.user.has_perm('core.sync_datasource', obj=datasource):
             raise PermissionDenied(_("This user does not have permission to synchronize this data source."))
             raise PermissionDenied(_("This user does not have permission to synchronize this data source."))
 
 
-        # Enqueue the sync job & update the DataSource's status
+        # Enqueue the sync job
         SyncDataSourceJob.enqueue(instance=datasource, user=request.user)
         SyncDataSourceJob.enqueue(instance=datasource, user=request.user)
-        datasource.status = DataSourceStatusChoices.QUEUED
-        DataSource.objects.filter(pk=datasource.pk).update(status=datasource.status)
 
 
         serializer = serializers.DataSourceSerializer(datasource, context={'request': request})
         serializer = serializers.DataSourceSerializer(datasource, context={'request': request})
 
 

+ 11 - 0
netbox/core/jobs.py

@@ -28,6 +28,17 @@ class SyncDataSourceJob(JobRunner):
     class Meta:
     class Meta:
         name = 'Synchronization'
         name = 'Synchronization'
 
 
+    @classmethod
+    def enqueue(cls, *args, **kwargs):
+        job = super().enqueue(*args, **kwargs)
+
+        # Update the DataSource's synchronization status to queued
+        if datasource := job.object:
+            datasource.status = DataSourceStatusChoices.QUEUED
+            DataSource.objects.filter(pk=datasource.pk).update(status=datasource.status)
+
+        return job
+
     def run(self, *args, **kwargs):
     def run(self, *args, **kwargs):
         datasource = DataSource.objects.get(pk=self.job.object_id)
         datasource = DataSource.objects.get(pk=self.job.object_id)
         self.logger.debug(f"Found DataSource ID {datasource.pk}")
         self.logger.debug(f"Found DataSource ID {datasource.pk}")

+ 1 - 6
netbox/core/views.py

@@ -34,7 +34,6 @@ from utilities.json import ConfigJSONEncoder
 from utilities.query import count_related
 from utilities.query import count_related
 from utilities.views import ContentTypePermissionRequiredMixin, GetRelatedModelsMixin, ViewTab, register_model_view
 from utilities.views import ContentTypePermissionRequiredMixin, GetRelatedModelsMixin, ViewTab, register_model_view
 from . import filtersets, forms, tables
 from . import filtersets, forms, tables
-from .choices import DataSourceStatusChoices
 from .jobs import SyncDataSourceJob
 from .jobs import SyncDataSourceJob
 from .models import *
 from .models import *
 from .plugins import get_catalog_plugins, get_local_plugins
 from .plugins import get_catalog_plugins, get_local_plugins
@@ -79,12 +78,8 @@ class DataSourceSyncView(BaseObjectView):
 
 
     def post(self, request, pk):
     def post(self, request, pk):
         datasource = get_object_or_404(self.queryset, pk=pk)
         datasource = get_object_or_404(self.queryset, pk=pk)
-
-        # Enqueue the sync job & update the DataSource's status
+        # Enqueue the sync job
         job = SyncDataSourceJob.enqueue(instance=datasource, user=request.user)
         job = SyncDataSourceJob.enqueue(instance=datasource, user=request.user)
-        datasource.status = DataSourceStatusChoices.QUEUED
-        DataSource.objects.filter(pk=datasource.pk).update(status=datasource.status)
-
         messages.success(
         messages.success(
             request,
             request,
             _("Queued job #{id} to sync {datasource}").format(id=job.pk, datasource=datasource)
             _("Queued job #{id} to sync {datasource}").format(id=job.pk, datasource=datasource)

+ 63 - 3
netbox/dcim/forms/bulk_import.py

@@ -1335,6 +1335,13 @@ class MACAddressImportForm(NetBoxModelImportForm):
 
 
 class CableImportForm(NetBoxModelImportForm):
 class CableImportForm(NetBoxModelImportForm):
     # Termination A
     # Termination A
+    side_a_site = CSVModelChoiceField(
+        label=_('Side A site'),
+        queryset=Site.objects.all(),
+        required=False,
+        to_field_name='name',
+        help_text=_('Site of parent device A (if any)'),
+    )
     side_a_device = CSVModelChoiceField(
     side_a_device = CSVModelChoiceField(
         label=_('Side A device'),
         label=_('Side A device'),
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
@@ -1353,6 +1360,13 @@ class CableImportForm(NetBoxModelImportForm):
     )
     )
 
 
     # Termination B
     # Termination B
+    side_b_site = CSVModelChoiceField(
+        label=_('Side B site'),
+        queryset=Site.objects.all(),
+        required=False,
+        to_field_name='name',
+        help_text=_('Site of parent device B (if any)'),
+    )
     side_b_device = CSVModelChoiceField(
     side_b_device = CSVModelChoiceField(
         label=_('Side B device'),
         label=_('Side B device'),
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
@@ -1396,14 +1410,39 @@ class CableImportForm(NetBoxModelImportForm):
         required=False,
         required=False,
         help_text=_('Length unit')
         help_text=_('Length unit')
     )
     )
+    color = forms.CharField(
+        label=_('Color'),
+        required=False,
+        max_length=16,
+        help_text=_('Color name (e.g. "Red") or hex code (e.g. "f44336")')
+    )
 
 
     class Meta:
     class Meta:
         model = Cable
         model = Cable
         fields = [
         fields = [
-            'side_a_device', 'side_a_type', 'side_a_name', 'side_b_device', 'side_b_type', 'side_b_name', 'type',
-            'status', 'tenant', 'label', 'color', 'length', 'length_unit', 'description', 'comments', 'tags',
+            'side_a_site', 'side_a_device', 'side_a_type', 'side_a_name', 'side_b_site', 'side_b_device', 'side_b_type',
+            'side_b_name', 'type', 'status', 'tenant', 'label', 'color', 'length', 'length_unit', 'description',
+            'comments', 'tags',
         ]
         ]
 
 
+    def __init__(self, data=None, *args, **kwargs):
+        super().__init__(data, *args, **kwargs)
+
+        if data:
+            # Limit choices for side_a_device to the assigned side_a_site
+            if side_a_site := data.get('side_a_site'):
+                side_a_device_params = {f'site__{self.fields["side_a_site"].to_field_name}': side_a_site}
+                self.fields['side_a_device'].queryset = self.fields['side_a_device'].queryset.filter(
+                    **side_a_device_params
+                )
+
+            # Limit choices for side_b_device to the assigned side_b_site
+            if side_b_site := data.get('side_b_site'):
+                side_b_device_params = {f'site__{self.fields["side_b_site"].to_field_name}': side_b_site}
+                self.fields['side_b_device'].queryset = self.fields['side_b_device'].queryset.filter(
+                    **side_b_device_params
+                )
+
     def _clean_side(self, side):
     def _clean_side(self, side):
         """
         """
         Derive a Cable's A/B termination objects.
         Derive a Cable's A/B termination objects.
@@ -1440,6 +1479,24 @@ class CableImportForm(NetBoxModelImportForm):
         setattr(self.instance, f'{side}_terminations', [termination_object])
         setattr(self.instance, f'{side}_terminations', [termination_object])
         return termination_object
         return termination_object
 
 
+    def _clean_color(self, color):
+        """
+        Derive a colors hex code
+
+        :param color: color as hex or color name
+        """
+        color_parsed = color.strip().lower()
+
+        for hex_code, label in ColorChoices.CHOICES:
+            if color.lower() == label.lower():
+                color_parsed = hex_code
+
+        if len(color_parsed) > 6:
+            raise forms.ValidationError(
+                _(f"{color} did not match any used color name and was longer than six characters: invalid hex.")
+            )
+        return color_parsed
+
     def clean_side_a_name(self):
     def clean_side_a_name(self):
         return self._clean_side('a')
         return self._clean_side('a')
 
 
@@ -1451,11 +1508,14 @@ class CableImportForm(NetBoxModelImportForm):
         length_unit = self.cleaned_data.get('length_unit', None)
         length_unit = self.cleaned_data.get('length_unit', None)
         return length_unit if length_unit is not None else ''
         return length_unit if length_unit is not None else ''
 
 
-
+    def clean_color(self):
+        color = self.cleaned_data.get('color', None)
+        return self._clean_color(color) if color is not None else ''
 #
 #
 # Virtual chassis
 # Virtual chassis
 #
 #
 
 
+
 class VirtualChassisImportForm(NetBoxModelImportForm):
 class VirtualChassisImportForm(NetBoxModelImportForm):
     master = CSVModelChoiceField(
     master = CSVModelChoiceField(
         label=_('Master'),
         label=_('Master'),

+ 30 - 1
netbox/dcim/svg/racks.py

@@ -3,6 +3,7 @@ import svgwrite
 from svgwrite.container import Hyperlink
 from svgwrite.container import Hyperlink
 from svgwrite.image import Image
 from svgwrite.image import Image
 from svgwrite.gradients import LinearGradient
 from svgwrite.gradients import LinearGradient
+from svgwrite.masking import ClipPath
 from svgwrite.shapes import Rect
 from svgwrite.shapes import Rect
 from svgwrite.text import Text
 from svgwrite.text import Text
 
 
@@ -67,6 +68,20 @@ def get_device_description(device):
     return description
     return description
 
 
 
 
+def truncate_text(text, width, font_size=15):
+    """
+    Truncate text to fit within the width of a rectangle.
+
+    :param text: The text to truncate
+    :param width: Width of rectangle
+    :param font_size: Font size (default is 15, ~0.875rem)
+    """
+    char_width = font_size * 0.6  # 0.6 is an approximation of the average character width in pixels
+    max_char = int(width / char_width)
+
+    return text if len(text) <= max_char else text[:max_char] + '...'
+
+
 class RackElevationSVG:
 class RackElevationSVG:
     """
     """
     Use this class to render a rack elevation as an SVG image.
     Use this class to render a rack elevation as an SVG image.
@@ -177,12 +192,26 @@ class RackElevationSVG:
         link = Hyperlink(href=f'{self.base_url}{device.get_absolute_url()}', target="_parent")
         link = Hyperlink(href=f'{self.base_url}{device.get_absolute_url()}', target="_parent")
         link.set_desc(description)
         link.set_desc(description)
 
 
+        # Create clipPath element
+        # This is necessary as fallback because the truncate_text method is an approximation
+        clip_id = f"clip-{device.id}"
+        clip_path = ClipPath(id=clip_id)
+        clip_path.add(Rect(coords, size))
+
+        self.drawing.defs.add(clip_path)
+
+        # Name to display
+        display_name = truncate_text(name, size[0])
+
         # Add rect element to hyperlink
         # Add rect element to hyperlink
         if color:
         if color:
             link.add(Rect(coords, size, style=f'fill: #{color}', class_=f'slot{css_extra}'))
             link.add(Rect(coords, size, style=f'fill: #{color}', class_=f'slot{css_extra}'))
         else:
         else:
             link.add(Rect(coords, size, class_=f'slot blocked{css_extra}'))
             link.add(Rect(coords, size, class_=f'slot blocked{css_extra}'))
-        link.add(Text(name, insert=text_coords, fill=text_color, class_=f'label{css_extra}'))
+        link.add(
+            Text(display_name, insert=text_coords, fill=text_color, clip_path=f"url(#{clip_id})",
+                 class_=f'label{css_extra}')
+        )
 
 
         # Embed device type image if provided
         # Embed device type image if provided
         if self.include_images and image:
         if self.include_images and image:

+ 23 - 11
netbox/dcim/tests/test_views.py

@@ -3266,17 +3266,27 @@ class CableTestCase(
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
 
 
-        site = Site.objects.create(name='Site 1', slug='site-1')
+        sites = (
+            Site(name='Site 1', slug='site-1'),
+            Site(name='Site 2', slug='site-2'),
+        )
+        Site.objects.bulk_create(sites)
         manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
         manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
         devicetype = DeviceType.objects.create(model='Device Type 1', manufacturer=manufacturer)
         devicetype = DeviceType.objects.create(model='Device Type 1', manufacturer=manufacturer)
         role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
         role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
         vc = VirtualChassis.objects.create(name='Virtual Chassis')
         vc = VirtualChassis.objects.create(name='Virtual Chassis')
 
 
+        # NOTE: By design, NetBox now allows for the creation of devices with the same name if they belong to
+        # different sites.
+        # The CSV test below demonstrates that devices with identical names on different sites can be created
+        # and referenced successfully.
         devices = (
         devices = (
-            Device(name='Device 1', site=site, device_type=devicetype, role=role),
-            Device(name='Device 2', site=site, device_type=devicetype, role=role),
-            Device(name='Device 3', site=site, device_type=devicetype, role=role),
-            Device(name='Device 4', site=site, device_type=devicetype, role=role),
+            # Create 'Device 1' assigned to 'Site 1'
+            Device(name='Device 1', site=sites[0], device_type=devicetype, role=role),
+            Device(name='Device 2', site=sites[0], device_type=devicetype, role=role),
+            Device(name='Device 3', site=sites[0], device_type=devicetype, role=role),
+            # Create 'Device 1' assigned to 'Site 2' (allowed since the site is different)
+            Device(name='Device 1', site=sites[1], device_type=devicetype, role=role),
         )
         )
         Device.objects.bulk_create(devices)
         Device.objects.bulk_create(devices)
 
 
@@ -3327,13 +3337,15 @@ class CableTestCase(
             'tags': [t.pk for t in tags],
             'tags': [t.pk for t in tags],
         }
         }
 
 
+        # Ensure that CSV bulk import supports assigning terminations from parent devices that share
+        # the same device name, provided those devices belong to different sites.
         cls.csv_data = (
         cls.csv_data = (
-            "side_a_device,side_a_type,side_a_name,side_b_device,side_b_type,side_b_name",
-            "Device 3,dcim.interface,Interface 1,Device 4,dcim.interface,Interface 1",
-            "Device 3,dcim.interface,Interface 2,Device 4,dcim.interface,Interface 2",
-            "Device 3,dcim.interface,Interface 3,Device 4,dcim.interface,Interface 3",
-            "Device 1,dcim.interface,Device 2 Interface,Device 4,dcim.interface,Interface 4",
-            "Device 1,dcim.interface,Device 3 Interface,Device 4,dcim.interface,Interface 5",
+            "side_a_site,side_a_device,side_a_type,side_a_name,side_b_site,side_b_device,side_b_type,side_b_name",
+            "Site 1,Device 3,dcim.interface,Interface 1,Site 2,Device 1,dcim.interface,Interface 1",
+            "Site 1,Device 3,dcim.interface,Interface 2,Site 2,Device 1,dcim.interface,Interface 2",
+            "Site 1,Device 3,dcim.interface,Interface 3,Site 2,Device 1,dcim.interface,Interface 3",
+            "Site 1,Device 1,dcim.interface,Device 2 Interface,Site 2,Device 1,dcim.interface,Interface 4",
+            "Site 1,Device 1,dcim.interface,Device 3 Interface,Site 2,Device 1,dcim.interface,Interface 5",
         )
         )
 
 
         cls.csv_update_data = (
         cls.csv_update_data = (

+ 3 - 1
netbox/extras/api/views.py

@@ -184,7 +184,9 @@ class TagViewSet(NetBoxModelViewSet):
 
 
 
 
 class TaggedItemViewSet(RetrieveModelMixin, ListModelMixin, BaseViewSet):
 class TaggedItemViewSet(RetrieveModelMixin, ListModelMixin, BaseViewSet):
-    queryset = TaggedItem.objects.prefetch_related('content_type', 'content_object', 'tag')
+    queryset = TaggedItem.objects.prefetch_related(
+        'content_type', 'content_object', 'tag'
+    ).order_by('tag__weight', 'tag__name')
     serializer_class = serializers.TaggedItemSerializer
     serializer_class = serializers.TaggedItemSerializer
     filterset_class = filtersets.TaggedItemFilterSet
     filterset_class = filtersets.TaggedItemFilterSet
 
 

+ 6 - 0
netbox/extras/constants.py

@@ -21,6 +21,12 @@ WEBHOOK_EVENT_TYPES = {
     JOB_ERRORED: 'job_ended',
     JOB_ERRORED: 'job_ended',
 }
 }
 
 
+# Jinja environment parameters which support path imports
+JINJA_ENV_PARAMS_WITH_PATH_IMPORT = (
+    'undefined',
+    'finalize',
+)
+
 # Dashboard
 # Dashboard
 DEFAULT_DASHBOARD = [
 DEFAULT_DASHBOARD = [
     {
     {

+ 15 - 4
netbox/extras/models/mixins.py

@@ -2,16 +2,17 @@ import importlib.abc
 import importlib.util
 import importlib.util
 import os
 import os
 import sys
 import sys
+
 from django.core.files.storage import storages
 from django.core.files.storage import storages
 from django.db import models
 from django.db import models
-from django.utils.translation import gettext_lazy as _
 from django.http import HttpResponse
 from django.http import HttpResponse
+from django.utils.module_loading import import_string
+from django.utils.translation import gettext_lazy as _
 
 
-from extras.constants import DEFAULT_MIME_TYPE
+from extras.constants import DEFAULT_MIME_TYPE, JINJA_ENV_PARAMS_WITH_PATH_IMPORT
 from extras.utils import filename_from_model, filename_from_object
 from extras.utils import filename_from_model, filename_from_object
 from utilities.jinja2 import render_jinja2
 from utilities.jinja2 import render_jinja2
 
 
-
 __all__ = (
 __all__ = (
     'PythonModuleMixin',
     'PythonModuleMixin',
     'RenderTemplateMixin',
     'RenderTemplateMixin',
@@ -125,12 +126,22 @@ class RenderTemplateMixin(models.Model):
             class_name=self.__class__
             class_name=self.__class__
         ))
         ))
 
 
+    def get_environment_params(self):
+        """
+        Pre-processing of any defined Jinja environment parameters (e.g. to support path resolution).
+        """
+        params = self.environment_params or {}
+        for name, value in params.items():
+            if name in JINJA_ENV_PARAMS_WITH_PATH_IMPORT and type(value) is str:
+                params[name] = import_string(value)
+        return params
+
     def render(self, context=None, queryset=None):
     def render(self, context=None, queryset=None):
         """
         """
         Render the template with the provided context. The context is passed to the Jinja2 environment as a dictionary.
         Render the template with the provided context. The context is passed to the Jinja2 environment as a dictionary.
         """
         """
         context = self.get_context(context=context, queryset=queryset)
         context = self.get_context(context=context, queryset=queryset)
-        env_params = self.environment_params or {}
+        env_params = self.get_environment_params()
         output = render_jinja2(self.template_code, context, env_params, getattr(self, 'data_file', None))
         output = render_jinja2(self.template_code, context, env_params, getattr(self, 'data_file', None))
 
 
         # Replace CRLF-style line terminators
         # Replace CRLF-style line terminators

+ 3 - 0
netbox/extras/models/tags.py

@@ -83,3 +83,6 @@ class TaggedItem(GenericTaggedItemBase):
         indexes = [models.Index(fields=["content_type", "object_id"])]
         indexes = [models.Index(fields=["content_type", "object_id"])]
         verbose_name = _('tagged item')
         verbose_name = _('tagged item')
         verbose_name_plural = _('tagged items')
         verbose_name_plural = _('tagged items')
+        # Note: while there is no ordering applied here (because it would basically be done on fields
+        # of the related `tag`), there is an ordering applied to extras.api.views.TaggedItemViewSet
+        # to allow for proper pagination.

+ 4 - 0
netbox/netbox/api/exceptions.py

@@ -12,3 +12,7 @@ class SerializerNotFound(Exception):
 
 
 class GraphQLTypeNotFound(Exception):
 class GraphQLTypeNotFound(Exception):
     pass
     pass
+
+
+class QuerySetNotOrdered(Exception):
+    pass

+ 7 - 0
netbox/netbox/api/pagination.py

@@ -1,6 +1,7 @@
 from django.db.models import QuerySet
 from django.db.models import QuerySet
 from rest_framework.pagination import LimitOffsetPagination
 from rest_framework.pagination import LimitOffsetPagination
 
 
+from netbox.api.exceptions import QuerySetNotOrdered
 from netbox.config import get_config
 from netbox.config import get_config
 
 
 
 
@@ -15,6 +16,12 @@ class OptionalLimitOffsetPagination(LimitOffsetPagination):
 
 
     def paginate_queryset(self, queryset, request, view=None):
     def paginate_queryset(self, queryset, request, view=None):
 
 
+        if isinstance(queryset, QuerySet) and not queryset.ordered:
+            raise QuerySetNotOrdered(
+                "Paginating over an unordered queryset is unreliable. Ensure that a minimal "
+                "ordering has been applied to the queryset for this API endpoint."
+            )
+
         if isinstance(queryset, QuerySet):
         if isinstance(queryset, QuerySet):
             self.count = self.get_queryset_count(queryset)
             self.count = self.get_queryset_count(queryset)
         else:
         else:

+ 42 - 0
netbox/netbox/tests/test_api.py

@@ -1,8 +1,13 @@
 import uuid
 import uuid
 
 
+from django.test import RequestFactory, TestCase
 from django.urls import reverse
 from django.urls import reverse
+from rest_framework.request import Request
 
 
+from netbox.api.exceptions import QuerySetNotOrdered
+from netbox.api.pagination import OptionalLimitOffsetPagination
 from utilities.testing import APITestCase
 from utilities.testing import APITestCase
+from users.models import Token
 
 
 
 
 class AppTest(APITestCase):
 class AppTest(APITestCase):
@@ -26,3 +31,40 @@ class AppTest(APITestCase):
         response = self.client.get(f'{url}?format=api', **self.header)
         response = self.client.get(f'{url}?format=api', **self.header)
 
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
+
+
+class OptionalLimitOffsetPaginationTest(TestCase):
+
+    def setUp(self):
+        self.paginator = OptionalLimitOffsetPagination()
+        self.factory = RequestFactory()
+
+    def _make_drf_request(self, path='/', query_params=None):
+        """Helper to create a proper DRF Request object"""
+        return Request(self.factory.get(path, query_params or {}))
+
+    def test_raises_exception_for_unordered_queryset(self):
+        """Should raise QuerySetNotOrdered for unordered QuerySet"""
+        queryset = Token.objects.all().order_by()
+        request = self._make_drf_request()
+
+        with self.assertRaises(QuerySetNotOrdered) as cm:
+            self.paginator.paginate_queryset(queryset, request)
+
+        error_msg = str(cm.exception)
+        self.assertIn("Paginating over an unordered queryset is unreliable", error_msg)
+        self.assertIn("Ensure that a minimal ordering has been applied", error_msg)
+
+    def test_allows_ordered_queryset(self):
+        """Should not raise exception for ordered QuerySet"""
+        queryset = Token.objects.all().order_by('created')
+        request = self._make_drf_request()
+
+        self.paginator.paginate_queryset(queryset, request)  # Should not raise exception
+
+    def test_allows_non_queryset_iterables(self):
+        """Should not raise exception for non-QuerySet iterables"""
+        iterable = [1, 2, 3, 4, 5]
+        request = self._make_drf_request()
+
+        self.paginator.paginate_queryset(iterable, request)  # Should not raise exception

File diff suppressed because it is too large
+ 0 - 0
netbox/project-static/dist/netbox.js


File diff suppressed because it is too large
+ 0 - 0
netbox/project-static/dist/netbox.js.map


+ 2 - 2
netbox/project-static/src/racks.ts

@@ -35,7 +35,7 @@ function showRackElements(
   selector: string,
   selector: string,
   elevation: HTMLObjectElement,
   elevation: HTMLObjectElement,
 ): void {
 ): void {
-  const elements = elevation.contentDocument?.querySelectorAll(selector) ?? [];
+  const elements = elevation.querySelectorAll(selector) ?? [];
   for (const element of elements) {
   for (const element of elements) {
     element.classList.remove('hidden');
     element.classList.remove('hidden');
   }
   }
@@ -45,7 +45,7 @@ function hideRackElements(
   selector: string,
   selector: string,
   elevation: HTMLObjectElement,
   elevation: HTMLObjectElement,
 ): void {
 ): void {
-  const elements = elevation.contentDocument?.querySelectorAll(selector) ?? [];
+  const elements = elevation.querySelectorAll(selector) ?? [];
   for (const element of elements) {
   for (const element of elements) {
     element.classList.add('hidden');
     element.classList.add('hidden');
   }
   }

+ 262 - 252
netbox/project-static/yarn.lock

@@ -19,135 +19,135 @@
   resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb"
   resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb"
   integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==
   integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==
 
 
-"@esbuild/aix-ppc64@0.25.6":
-  version "0.25.6"
-  resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz#164b19122e2ed54f85469df9dea98ddb01d5e79e"
-  integrity sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==
-
-"@esbuild/android-arm64@0.25.6":
-  version "0.25.6"
-  resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.6.tgz#8f539e7def848f764f6432598e51cc3820fde3a5"
-  integrity sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==
-
-"@esbuild/android-arm@0.25.6":
-  version "0.25.6"
-  resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.6.tgz#4ceb0f40113e9861169be83e2a670c260dd234ff"
-  integrity sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==
-
-"@esbuild/android-x64@0.25.6":
-  version "0.25.6"
-  resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.6.tgz#ad4f280057622c25fe985c08999443a195dc63a8"
-  integrity sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==
-
-"@esbuild/darwin-arm64@0.25.6":
-  version "0.25.6"
-  resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.6.tgz#d1f04027396b3d6afc96bacd0d13167dfd9f01f7"
-  integrity sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==
-
-"@esbuild/darwin-x64@0.25.6":
-  version "0.25.6"
-  resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.6.tgz#2b4a6cedb799f635758d7832d75b23772c8ef68f"
-  integrity sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==
-
-"@esbuild/freebsd-arm64@0.25.6":
-  version "0.25.6"
-  resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.6.tgz#a26266cc97dd78dc3c3f3d6788b1b83697b1055d"
-  integrity sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==
-
-"@esbuild/freebsd-x64@0.25.6":
-  version "0.25.6"
-  resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.6.tgz#9feb8e826735c568ebfd94859b22a3fbb6a9bdd2"
-  integrity sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==
-
-"@esbuild/linux-arm64@0.25.6":
-  version "0.25.6"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.6.tgz#c07cbed8e249f4c28e7f32781d36fc4695293d28"
-  integrity sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==
-
-"@esbuild/linux-arm@0.25.6":
-  version "0.25.6"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.6.tgz#d6e2cd8ef3196468065d41f13fa2a61aaa72644a"
-  integrity sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==
-
-"@esbuild/linux-ia32@0.25.6":
-  version "0.25.6"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.6.tgz#3e682bd47c4eddcc4b8f1393dfc8222482f17997"
-  integrity sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==
-
-"@esbuild/linux-loong64@0.25.6":
-  version "0.25.6"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.6.tgz#473f5ea2e52399c08ad4cd6b12e6dbcddd630f05"
-  integrity sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==
-
-"@esbuild/linux-mips64el@0.25.6":
-  version "0.25.6"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.6.tgz#9960631c9fd61605b0939c19043acf4ef2b51718"
-  integrity sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==
-
-"@esbuild/linux-ppc64@0.25.6":
-  version "0.25.6"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.6.tgz#477cbf8bb04aa034b94f362c32c86b5c31db8d3e"
-  integrity sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==
-
-"@esbuild/linux-riscv64@0.25.6":
-  version "0.25.6"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.6.tgz#bcdb46c8fb8e93aa779e9a0a62cd4ac00dcac626"
-  integrity sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==
-
-"@esbuild/linux-s390x@0.25.6":
-  version "0.25.6"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.6.tgz#f412cf5fdf0aea849ff51c73fd817c6c0234d46d"
-  integrity sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==
-
-"@esbuild/linux-x64@0.25.6":
-  version "0.25.6"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz#d8233c09b5ebc0c855712dc5eeb835a3a3341108"
-  integrity sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==
-
-"@esbuild/netbsd-arm64@0.25.6":
-  version "0.25.6"
-  resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.6.tgz#f51ae8dd1474172e73cf9cbaf8a38d1c72dd8f1a"
-  integrity sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==
-
-"@esbuild/netbsd-x64@0.25.6":
-  version "0.25.6"
-  resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.6.tgz#a267538602c0e50a858cf41dcfe5d8036f8da8e7"
-  integrity sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==
-
-"@esbuild/openbsd-arm64@0.25.6":
-  version "0.25.6"
-  resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.6.tgz#a51be60c425b85c216479b8c344ad0511635f2d2"
-  integrity sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==
-
-"@esbuild/openbsd-x64@0.25.6":
-  version "0.25.6"
-  resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.6.tgz#7e4a743c73f75562e29223ba69d0be6c9c9008da"
-  integrity sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==
-
-"@esbuild/openharmony-arm64@0.25.6":
-  version "0.25.6"
-  resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.6.tgz#2087a5028f387879154ebf44bdedfafa17682e5b"
-  integrity sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==
-
-"@esbuild/sunos-x64@0.25.6":
-  version "0.25.6"
-  resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.6.tgz#56531f861723ea0dc6283a2bb8837304223cb736"
-  integrity sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==
-
-"@esbuild/win32-arm64@0.25.6":
-  version "0.25.6"
-  resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.6.tgz#f4989f033deac6fae323acff58764fa8bc01436e"
-  integrity sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==
-
-"@esbuild/win32-ia32@0.25.6":
-  version "0.25.6"
-  resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.6.tgz#b260e9df71e3939eb33925076d39f63cec7d1525"
-  integrity sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==
-
-"@esbuild/win32-x64@0.25.6":
-  version "0.25.6"
-  resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.6.tgz#4276edd5c105bc28b11c6a1f76fb9d29d1bd25c1"
-  integrity sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==
+"@esbuild/aix-ppc64@0.25.8":
+  version "0.25.8"
+  resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz#a1414903bb38027382f85f03dda6065056757727"
+  integrity sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==
+
+"@esbuild/android-arm64@0.25.8":
+  version "0.25.8"
+  resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz#c859994089e9767224269884061f89dae6fb51c6"
+  integrity sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==
+
+"@esbuild/android-arm@0.25.8":
+  version "0.25.8"
+  resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.8.tgz#96a8f2ca91c6cd29ea90b1af79d83761c8ba0059"
+  integrity sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==
+
+"@esbuild/android-x64@0.25.8":
+  version "0.25.8"
+  resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.8.tgz#a3a626c4fec4a024a9fa8c7679c39996e92916f0"
+  integrity sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==
+
+"@esbuild/darwin-arm64@0.25.8":
+  version "0.25.8"
+  resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz#a5e1252ca2983d566af1c0ea39aded65736fc66d"
+  integrity sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==
+
+"@esbuild/darwin-x64@0.25.8":
+  version "0.25.8"
+  resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz#5271b0df2bb12ce8df886704bfdd1c7cc01385d2"
+  integrity sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==
+
+"@esbuild/freebsd-arm64@0.25.8":
+  version "0.25.8"
+  resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz#d0a0e7fdf19733b8bb1566b81df1aa0bb7e46ada"
+  integrity sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==
+
+"@esbuild/freebsd-x64@0.25.8":
+  version "0.25.8"
+  resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz#2de8b2e0899d08f1cb1ef3128e159616e7e85343"
+  integrity sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==
+
+"@esbuild/linux-arm64@0.25.8":
+  version "0.25.8"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz#a4209efadc0c2975716458484a4e90c237c48ae9"
+  integrity sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==
+
+"@esbuild/linux-arm@0.25.8":
+  version "0.25.8"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz#ccd9e291c24cd8d9142d819d463e2e7200d25b19"
+  integrity sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==
+
+"@esbuild/linux-ia32@0.25.8":
+  version "0.25.8"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz#006ad1536d0c2b28fb3a1cf0b53bcb85aaf92c4d"
+  integrity sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==
+
+"@esbuild/linux-loong64@0.25.8":
+  version "0.25.8"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz#127b3fbfb2c2e08b1397e985932f718f09a8f5c4"
+  integrity sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==
+
+"@esbuild/linux-mips64el@0.25.8":
+  version "0.25.8"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz#837d1449517791e3fa7d82675a2d06d9f56cb340"
+  integrity sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==
+
+"@esbuild/linux-ppc64@0.25.8":
+  version "0.25.8"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz#aa2e3bd93ab8df084212f1895ca4b03c42d9e0fe"
+  integrity sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==
+
+"@esbuild/linux-riscv64@0.25.8":
+  version "0.25.8"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz#a340620e31093fef72767dd28ab04214b3442083"
+  integrity sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==
+
+"@esbuild/linux-s390x@0.25.8":
+  version "0.25.8"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz#ddfed266c8c13f5efb3105a0cd47f6dcd0e79e71"
+  integrity sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==
+
+"@esbuild/linux-x64@0.25.8":
+  version "0.25.8"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz#9a4f78c75c051e8c060183ebb39a269ba936a2ac"
+  integrity sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==
+
+"@esbuild/netbsd-arm64@0.25.8":
+  version "0.25.8"
+  resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz#902c80e1d678047926387230bc037e63e00697d0"
+  integrity sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==
+
+"@esbuild/netbsd-x64@0.25.8":
+  version "0.25.8"
+  resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz#2d9eb4692add2681ff05a14ce99de54fbed7079c"
+  integrity sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==
+
+"@esbuild/openbsd-arm64@0.25.8":
+  version "0.25.8"
+  resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz#89c3b998c6de739db38ab7fb71a8a76b3fa84a45"
+  integrity sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==
+
+"@esbuild/openbsd-x64@0.25.8":
+  version "0.25.8"
+  resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz#2f01615cf472b0e48c077045cfd96b5c149365cc"
+  integrity sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==
+
+"@esbuild/openharmony-arm64@0.25.8":
+  version "0.25.8"
+  resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz#a201f720cd2c3ebf9a6033fcc3feb069a54b509a"
+  integrity sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==
+
+"@esbuild/sunos-x64@0.25.8":
+  version "0.25.8"
+  resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz#07046c977985a3334667f19e6ab3a01a80862afb"
+  integrity sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==
+
+"@esbuild/win32-arm64@0.25.8":
+  version "0.25.8"
+  resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz#4a5470caf0d16127c05d4833d4934213c69392d1"
+  integrity sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==
+
+"@esbuild/win32-ia32@0.25.8":
+  version "0.25.8"
+  resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz#3de3e8470b7b328d99dbc3e9ec1eace207e5bbc4"
+  integrity sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==
+
+"@esbuild/win32-x64@0.25.8":
+  version "0.25.8"
+  resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz#610d7ea539d2fcdbe39237b5cc175eb2c4451f9c"
+  integrity sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==
 
 
 "@eslint-community/eslint-utils@^4.2.0":
 "@eslint-community/eslint-utils@^4.2.0":
   version "4.4.0"
   version "4.4.0"
@@ -163,7 +163,12 @@
   dependencies:
   dependencies:
     eslint-visitor-keys "^3.4.3"
     eslint-visitor-keys "^3.4.3"
 
 
-"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.6.1":
+"@eslint-community/regexpp@^4.10.0":
+  version "4.12.1"
+  resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0"
+  integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==
+
+"@eslint-community/regexpp@^4.6.1":
   version "4.11.0"
   version "4.11.0"
   resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.11.0.tgz#b0ffd0312b4a3fd2d6f77237e7248a5ad3a680ae"
   resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.11.0.tgz#b0ffd0312b4a3fd2d6f77237e7248a5ad3a680ae"
   integrity sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==
   integrity sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==
@@ -464,10 +469,10 @@
     "@parcel/watcher-win32-ia32" "2.4.1"
     "@parcel/watcher-win32-ia32" "2.4.1"
     "@parcel/watcher-win32-x64" "2.4.1"
     "@parcel/watcher-win32-x64" "2.4.1"
 
 
-"@pkgr/core@^0.2.4":
-  version "0.2.7"
-  resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.2.7.tgz#eb5014dfd0b03e7f3ba2eeeff506eed89b028058"
-  integrity sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==
+"@pkgr/core@^0.2.9":
+  version "0.2.9"
+  resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.2.9.tgz#d229a7b7f9dac167a156992ef23c7f023653f53b"
+  integrity sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==
 
 
 "@popperjs/core@^2.11.6", "@popperjs/core@^2.11.8", "@popperjs/core@^2.9.2":
 "@popperjs/core@^2.11.6", "@popperjs/core@^2.11.8", "@popperjs/core@^2.9.2":
   version "2.11.8"
   version "2.11.8"
@@ -840,78 +845,78 @@
     "@types/estree" "*"
     "@types/estree" "*"
 
 
 "@typescript-eslint/eslint-plugin@^8.37.0":
 "@typescript-eslint/eslint-plugin@^8.37.0":
-  version "8.37.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.37.0.tgz#332392883f936137cd6252c8eb236d298e514e70"
-  integrity sha512-jsuVWeIkb6ggzB+wPCsR4e6loj+rM72ohW6IBn2C+5NCvfUVY8s33iFPySSVXqtm5Hu29Ne/9bnA0JmyLmgenA==
+  version "8.38.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz#6e5220d16f2691ab6d983c1737dd5b36e17641b7"
+  integrity sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==
   dependencies:
   dependencies:
     "@eslint-community/regexpp" "^4.10.0"
     "@eslint-community/regexpp" "^4.10.0"
-    "@typescript-eslint/scope-manager" "8.37.0"
-    "@typescript-eslint/type-utils" "8.37.0"
-    "@typescript-eslint/utils" "8.37.0"
-    "@typescript-eslint/visitor-keys" "8.37.0"
+    "@typescript-eslint/scope-manager" "8.38.0"
+    "@typescript-eslint/type-utils" "8.38.0"
+    "@typescript-eslint/utils" "8.38.0"
+    "@typescript-eslint/visitor-keys" "8.38.0"
     graphemer "^1.4.0"
     graphemer "^1.4.0"
     ignore "^7.0.0"
     ignore "^7.0.0"
     natural-compare "^1.4.0"
     natural-compare "^1.4.0"
     ts-api-utils "^2.1.0"
     ts-api-utils "^2.1.0"
 
 
 "@typescript-eslint/parser@^8.37.0":
 "@typescript-eslint/parser@^8.37.0":
-  version "8.37.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.37.0.tgz#b87f6b61e25ad5cc5bbf8baf809b8da889c89804"
-  integrity sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA==
-  dependencies:
-    "@typescript-eslint/scope-manager" "8.37.0"
-    "@typescript-eslint/types" "8.37.0"
-    "@typescript-eslint/typescript-estree" "8.37.0"
-    "@typescript-eslint/visitor-keys" "8.37.0"
+  version "8.38.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.38.0.tgz#6723a5ea881e1777956b1045cba30be5ea838293"
+  integrity sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==
+  dependencies:
+    "@typescript-eslint/scope-manager" "8.38.0"
+    "@typescript-eslint/types" "8.38.0"
+    "@typescript-eslint/typescript-estree" "8.38.0"
+    "@typescript-eslint/visitor-keys" "8.38.0"
     debug "^4.3.4"
     debug "^4.3.4"
 
 
-"@typescript-eslint/project-service@8.37.0":
-  version "8.37.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.37.0.tgz#0594352e32a4ac9258591b88af77b5653800cdfe"
-  integrity sha512-BIUXYsbkl5A1aJDdYJCBAo8rCEbAvdquQ8AnLb6z5Lp1u3x5PNgSSx9A/zqYc++Xnr/0DVpls8iQ2cJs/izTXA==
+"@typescript-eslint/project-service@8.38.0":
+  version "8.38.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.38.0.tgz#4900771f943163027fd7d2020a062892056b5e2f"
+  integrity sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==
   dependencies:
   dependencies:
-    "@typescript-eslint/tsconfig-utils" "^8.37.0"
-    "@typescript-eslint/types" "^8.37.0"
+    "@typescript-eslint/tsconfig-utils" "^8.38.0"
+    "@typescript-eslint/types" "^8.38.0"
     debug "^4.3.4"
     debug "^4.3.4"
 
 
-"@typescript-eslint/scope-manager@8.37.0":
-  version "8.37.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.37.0.tgz#a31a3c80ca2ef4ed58de13742debb692e7d4c0a4"
-  integrity sha512-0vGq0yiU1gbjKob2q691ybTg9JX6ShiVXAAfm2jGf3q0hdP6/BruaFjL/ManAR/lj05AvYCH+5bbVo0VtzmjOA==
+"@typescript-eslint/scope-manager@8.38.0":
+  version "8.38.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.38.0.tgz#5a0efcb5c9cf6e4121b58f87972f567c69529226"
+  integrity sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==
   dependencies:
   dependencies:
-    "@typescript-eslint/types" "8.37.0"
-    "@typescript-eslint/visitor-keys" "8.37.0"
+    "@typescript-eslint/types" "8.38.0"
+    "@typescript-eslint/visitor-keys" "8.38.0"
 
 
-"@typescript-eslint/tsconfig-utils@8.37.0", "@typescript-eslint/tsconfig-utils@^8.37.0":
-  version "8.37.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.37.0.tgz#47a2760d265c6125f8e7864bc5c8537cad2bd053"
-  integrity sha512-1/YHvAVTimMM9mmlPvTec9NP4bobA1RkDbMydxG8omqwJJLEW/Iy2C4adsAESIXU3WGLXFHSZUU+C9EoFWl4Zg==
+"@typescript-eslint/tsconfig-utils@8.38.0", "@typescript-eslint/tsconfig-utils@^8.38.0":
+  version "8.38.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.38.0.tgz#6de4ce224a779601a8df667db56527255c42c4d0"
+  integrity sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==
 
 
-"@typescript-eslint/type-utils@8.37.0":
-  version "8.37.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.37.0.tgz#2a682e4c6ff5886712dad57e9787b5e417124507"
-  integrity sha512-SPkXWIkVZxhgwSwVq9rqj/4VFo7MnWwVaRNznfQDc/xPYHjXnPfLWn+4L6FF1cAz6e7dsqBeMawgl7QjUMj4Ow==
+"@typescript-eslint/type-utils@8.38.0":
+  version "8.38.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.38.0.tgz#a56cd84765fa6ec135fe252b5db61e304403a85b"
+  integrity sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg==
   dependencies:
   dependencies:
-    "@typescript-eslint/types" "8.37.0"
-    "@typescript-eslint/typescript-estree" "8.37.0"
-    "@typescript-eslint/utils" "8.37.0"
+    "@typescript-eslint/types" "8.38.0"
+    "@typescript-eslint/typescript-estree" "8.38.0"
+    "@typescript-eslint/utils" "8.38.0"
     debug "^4.3.4"
     debug "^4.3.4"
     ts-api-utils "^2.1.0"
     ts-api-utils "^2.1.0"
 
 
-"@typescript-eslint/types@8.37.0", "@typescript-eslint/types@^8.37.0":
-  version "8.37.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.37.0.tgz#09517aa9625eb3c68941dde3ac8835740587b6ff"
-  integrity sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ==
+"@typescript-eslint/types@8.38.0", "@typescript-eslint/types@^8.38.0":
+  version "8.38.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.38.0.tgz#297351c994976b93c82ac0f0e206c8143aa82529"
+  integrity sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==
 
 
-"@typescript-eslint/typescript-estree@8.37.0":
-  version "8.37.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.37.0.tgz#a07e4574d8e6e4355a558f61323730c987f5fcbc"
-  integrity sha512-zuWDMDuzMRbQOM+bHyU4/slw27bAUEcKSKKs3hcv2aNnc/tvE/h7w60dwVw8vnal2Pub6RT1T7BI8tFZ1fE+yg==
+"@typescript-eslint/typescript-estree@8.38.0":
+  version "8.38.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.38.0.tgz#82262199eb6778bba28a319e25ad05b1158957df"
+  integrity sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==
   dependencies:
   dependencies:
-    "@typescript-eslint/project-service" "8.37.0"
-    "@typescript-eslint/tsconfig-utils" "8.37.0"
-    "@typescript-eslint/types" "8.37.0"
-    "@typescript-eslint/visitor-keys" "8.37.0"
+    "@typescript-eslint/project-service" "8.38.0"
+    "@typescript-eslint/tsconfig-utils" "8.38.0"
+    "@typescript-eslint/types" "8.38.0"
+    "@typescript-eslint/visitor-keys" "8.38.0"
     debug "^4.3.4"
     debug "^4.3.4"
     fast-glob "^3.3.2"
     fast-glob "^3.3.2"
     is-glob "^4.0.3"
     is-glob "^4.0.3"
@@ -919,22 +924,22 @@
     semver "^7.6.0"
     semver "^7.6.0"
     ts-api-utils "^2.1.0"
     ts-api-utils "^2.1.0"
 
 
-"@typescript-eslint/utils@8.37.0":
-  version "8.37.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.37.0.tgz#189ea59b2709f5d898614611f091a776751ee335"
-  integrity sha512-TSFvkIW6gGjN2p6zbXo20FzCABbyUAuq6tBvNRGsKdsSQ6a7rnV6ADfZ7f4iI3lIiXc4F4WWvtUfDw9CJ9pO5A==
+"@typescript-eslint/utils@8.38.0":
+  version "8.38.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.38.0.tgz#5f10159899d30eb92ba70e642ca6f754bddbf15a"
+  integrity sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==
   dependencies:
   dependencies:
     "@eslint-community/eslint-utils" "^4.7.0"
     "@eslint-community/eslint-utils" "^4.7.0"
-    "@typescript-eslint/scope-manager" "8.37.0"
-    "@typescript-eslint/types" "8.37.0"
-    "@typescript-eslint/typescript-estree" "8.37.0"
+    "@typescript-eslint/scope-manager" "8.38.0"
+    "@typescript-eslint/types" "8.38.0"
+    "@typescript-eslint/typescript-estree" "8.38.0"
 
 
-"@typescript-eslint/visitor-keys@8.37.0":
-  version "8.37.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.37.0.tgz#cdb6a6bd3e8d6dd69bd70c1bdda36e2d18737455"
-  integrity sha512-YzfhzcTnZVPiLfP/oeKtDp2evwvHLMe0LOy7oe+hb9KKIumLNohYS9Hgp1ifwpu42YWxhZE8yieggz6JpqO/1w==
+"@typescript-eslint/visitor-keys@8.38.0":
+  version "8.38.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.38.0.tgz#a9765a527b082cb8fc60fd8a16e47c7ad5b60ea5"
+  integrity sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==
   dependencies:
   dependencies:
-    "@typescript-eslint/types" "8.37.0"
+    "@typescript-eslint/types" "8.38.0"
     eslint-visitor-keys "^4.2.1"
     eslint-visitor-keys "^4.2.1"
 
 
 "@ungap/structured-clone@^1.2.0":
 "@ungap/structured-clone@^1.2.0":
@@ -1120,9 +1125,9 @@ brace-expansion@^1.1.7:
     concat-map "0.0.1"
     concat-map "0.0.1"
 
 
 brace-expansion@^2.0.1:
 brace-expansion@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae"
-  integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.2.tgz#54fc53237a613d854c7bd37463aad17df87214e7"
+  integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==
   dependencies:
   dependencies:
     balanced-match "^1.0.0"
     balanced-match "^1.0.0"
 
 
@@ -1344,11 +1349,11 @@ debug@^4.3.1, debug@^4.3.2, debug@^4.3.5:
     ms "2.1.2"
     ms "2.1.2"
 
 
 debug@^4.3.4:
 debug@^4.3.4:
-  version "4.3.4"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
-  integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
+  version "4.4.1"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b"
+  integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==
   dependencies:
   dependencies:
-    ms "2.1.2"
+    ms "^2.1.3"
 
 
 decode-uri-component@^0.4.1:
 decode-uri-component@^0.4.1:
   version "0.4.1"
   version "0.4.1"
@@ -1633,36 +1638,36 @@ esbuild-sass-plugin@^3.3.1:
     sass "^1.71.1"
     sass "^1.71.1"
 
 
 esbuild@^0.25.6:
 esbuild@^0.25.6:
-  version "0.25.6"
-  resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.6.tgz#9b82a3db2fa131aec069ab040fd57ed0a880cdcd"
-  integrity sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==
+  version "0.25.8"
+  resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.8.tgz#482d42198b427c9c2f3a81b63d7663aecb1dda07"
+  integrity sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==
   optionalDependencies:
   optionalDependencies:
-    "@esbuild/aix-ppc64" "0.25.6"
-    "@esbuild/android-arm" "0.25.6"
-    "@esbuild/android-arm64" "0.25.6"
-    "@esbuild/android-x64" "0.25.6"
-    "@esbuild/darwin-arm64" "0.25.6"
-    "@esbuild/darwin-x64" "0.25.6"
-    "@esbuild/freebsd-arm64" "0.25.6"
-    "@esbuild/freebsd-x64" "0.25.6"
-    "@esbuild/linux-arm" "0.25.6"
-    "@esbuild/linux-arm64" "0.25.6"
-    "@esbuild/linux-ia32" "0.25.6"
-    "@esbuild/linux-loong64" "0.25.6"
-    "@esbuild/linux-mips64el" "0.25.6"
-    "@esbuild/linux-ppc64" "0.25.6"
-    "@esbuild/linux-riscv64" "0.25.6"
-    "@esbuild/linux-s390x" "0.25.6"
-    "@esbuild/linux-x64" "0.25.6"
-    "@esbuild/netbsd-arm64" "0.25.6"
-    "@esbuild/netbsd-x64" "0.25.6"
-    "@esbuild/openbsd-arm64" "0.25.6"
-    "@esbuild/openbsd-x64" "0.25.6"
-    "@esbuild/openharmony-arm64" "0.25.6"
-    "@esbuild/sunos-x64" "0.25.6"
-    "@esbuild/win32-arm64" "0.25.6"
-    "@esbuild/win32-ia32" "0.25.6"
-    "@esbuild/win32-x64" "0.25.6"
+    "@esbuild/aix-ppc64" "0.25.8"
+    "@esbuild/android-arm" "0.25.8"
+    "@esbuild/android-arm64" "0.25.8"
+    "@esbuild/android-x64" "0.25.8"
+    "@esbuild/darwin-arm64" "0.25.8"
+    "@esbuild/darwin-x64" "0.25.8"
+    "@esbuild/freebsd-arm64" "0.25.8"
+    "@esbuild/freebsd-x64" "0.25.8"
+    "@esbuild/linux-arm" "0.25.8"
+    "@esbuild/linux-arm64" "0.25.8"
+    "@esbuild/linux-ia32" "0.25.8"
+    "@esbuild/linux-loong64" "0.25.8"
+    "@esbuild/linux-mips64el" "0.25.8"
+    "@esbuild/linux-ppc64" "0.25.8"
+    "@esbuild/linux-riscv64" "0.25.8"
+    "@esbuild/linux-s390x" "0.25.8"
+    "@esbuild/linux-x64" "0.25.8"
+    "@esbuild/netbsd-arm64" "0.25.8"
+    "@esbuild/netbsd-x64" "0.25.8"
+    "@esbuild/openbsd-arm64" "0.25.8"
+    "@esbuild/openbsd-x64" "0.25.8"
+    "@esbuild/openharmony-arm64" "0.25.8"
+    "@esbuild/sunos-x64" "0.25.8"
+    "@esbuild/win32-arm64" "0.25.8"
+    "@esbuild/win32-ia32" "0.25.8"
+    "@esbuild/win32-x64" "0.25.8"
 
 
 escape-string-regexp@^4.0.0:
 escape-string-regexp@^4.0.0:
   version "4.0.0"
   version "4.0.0"
@@ -1737,9 +1742,9 @@ eslint-plugin-import@^2.32.0:
     tsconfig-paths "^3.15.0"
     tsconfig-paths "^3.15.0"
 
 
 eslint-plugin-prettier@^5.5.1:
 eslint-plugin-prettier@^5.5.1:
-  version "5.5.1"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.1.tgz#470820964de9aedb37e9ce62c3266d2d26d08d15"
-  integrity sha512-dobTkHT6XaEVOo8IO90Q4DOSxnm3Y151QxPJlM/vKC0bVy+d6cVWQZLlFiuZPP0wS6vZwSKeJgKkcS+KfMBlRw==
+  version "5.5.3"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.3.tgz#1f88e9220a72ac8be171eec5f9d4e4d529b5f4a0"
+  integrity sha512-NAdMYww51ehKfDyDhv59/eIItUVzU0Io9H2E8nHNGKEeeqlnci+1gCvrHib6EmZdf6GxF+LCV5K7UC65Ezvw7w==
   dependencies:
   dependencies:
     prettier-linter-helpers "^1.0.0"
     prettier-linter-helpers "^1.0.0"
     synckit "^0.11.7"
     synckit "^0.11.7"
@@ -1850,15 +1855,15 @@ fast-diff@^1.1.2:
   integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==
   integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==
 
 
 fast-glob@^3.3.2:
 fast-glob@^3.3.2:
-  version "3.3.2"
-  resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129"
-  integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==
+  version "3.3.3"
+  resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818"
+  integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==
   dependencies:
   dependencies:
     "@nodelib/fs.stat" "^2.0.2"
     "@nodelib/fs.stat" "^2.0.2"
     "@nodelib/fs.walk" "^1.2.3"
     "@nodelib/fs.walk" "^1.2.3"
     glob-parent "^5.1.2"
     glob-parent "^5.1.2"
     merge2 "^1.3.0"
     merge2 "^1.3.0"
-    micromatch "^4.0.4"
+    micromatch "^4.0.8"
 
 
 fast-json-stable-stringify@^2.0.0:
 fast-json-stable-stringify@^2.0.0:
   version "2.1.0"
   version "2.1.0"
@@ -1871,9 +1876,9 @@ fast-levenshtein@^2.0.6:
   integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
   integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
 
 
 fastq@^1.6.0:
 fastq@^1.6.0:
-  version "1.17.1"
-  resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47"
-  integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==
+  version "1.19.1"
+  resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.19.1.tgz#d50eaba803c8846a883c16492821ebcd2cda55f5"
+  integrity sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==
   dependencies:
   dependencies:
     reusify "^1.0.4"
     reusify "^1.0.4"
 
 
@@ -2741,7 +2746,7 @@ meros@^1.1.4:
   resolved "https://registry.yarnpkg.com/meros/-/meros-1.3.0.tgz#c617d2092739d55286bf618129280f362e6242f2"
   resolved "https://registry.yarnpkg.com/meros/-/meros-1.3.0.tgz#c617d2092739d55286bf618129280f362e6242f2"
   integrity sha512-2BNGOimxEz5hmjUG2FwoxCt5HN7BXdaWyFqEwxPTrJzVdABtrL4TiHTcsWSFAxPQ/tOnEaQEJh3qWq71QRMY+w==
   integrity sha512-2BNGOimxEz5hmjUG2FwoxCt5HN7BXdaWyFqEwxPTrJzVdABtrL4TiHTcsWSFAxPQ/tOnEaQEJh3qWq71QRMY+w==
 
 
-micromatch@^4.0.4, micromatch@^4.0.5:
+micromatch@^4.0.5, micromatch@^4.0.8:
   version "4.0.8"
   version "4.0.8"
   resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
   resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
   integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
   integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
@@ -2773,7 +2778,7 @@ ms@2.1.2:
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
   integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
   integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
 
 
-ms@^2.1.1:
+ms@^2.1.1, ms@^2.1.3:
   version "2.1.3"
   version "2.1.3"
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
   integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
   integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
@@ -3110,9 +3115,9 @@ resolve@^1.22.4, resolve@^1.22.8:
     supports-preserve-symlinks-flag "^1.0.0"
     supports-preserve-symlinks-flag "^1.0.0"
 
 
 reusify@^1.0.4:
 reusify@^1.0.4:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
-  integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.1.0.tgz#0fe13b9522e1473f51b558ee796e08f11f9b489f"
+  integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==
 
 
 rimraf@^3.0.2:
 rimraf@^3.0.2:
   version "3.0.2"
   version "3.0.2"
@@ -3217,7 +3222,12 @@ semver@^6.3.1:
   resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
   resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
   integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
   integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
 
 
-semver@^7.6.0, semver@^7.6.3:
+semver@^7.6.0:
+  version "7.7.2"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58"
+  integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
+
+semver@^7.6.3:
   version "7.6.3"
   version "7.6.3"
   resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143"
   resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143"
   integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==
   integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==
@@ -3430,11 +3440,11 @@ supports-preserve-symlinks-flag@^1.0.0:
   integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
   integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
 
 
 synckit@^0.11.7:
 synckit@^0.11.7:
-  version "0.11.8"
-  resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.11.8.tgz#b2aaae998a4ef47ded60773ad06e7cb821f55457"
-  integrity sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==
+  version "0.11.11"
+  resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.11.11.tgz#c0b619cf258a97faa209155d9cd1699b5c998cb0"
+  integrity sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==
   dependencies:
   dependencies:
-    "@pkgr/core" "^0.2.4"
+    "@pkgr/core" "^0.2.9"
 
 
 tapable@^2.2.0:
 tapable@^2.2.0:
   version "2.2.1"
   version "2.2.1"

+ 2 - 2
netbox/release.yaml

@@ -1,3 +1,3 @@
-version: "4.3.4"
+version: "4.3.5"
 edition: "Community"
 edition: "Community"
-published: "2025-07-15"
+published: "2025-07-29"

+ 3 - 3
netbox/templates/base/layout.html

@@ -55,7 +55,7 @@ Blocks:
           {# Release info #}
           {# Release info #}
           <div class="text-muted text-center fs-5 my-3">
           <div class="text-muted text-center fs-5 my-3">
             {{ settings.RELEASE.name }}
             {{ settings.RELEASE.name }}
-            {% if not settings.RELEASE.features.commercial %}
+            {% if not settings.RELEASE.features.commercial and not settings.ISOLATED_DEPLOYMENT %}
               <div>
               <div>
                 <a href="https://netboxlabs.com/netbox-cloud/" class="text-muted">{% trans "Get" %} Cloud</a> |
                 <a href="https://netboxlabs.com/netbox-cloud/" class="text-muted">{% trans "Get" %} Cloud</a> |
                 <a href="https://netboxlabs.com/netbox-enterprise/" class="text-muted">{% trans "Get" %} Enterprise</a>
                 <a href="https://netboxlabs.com/netbox-enterprise/" class="text-muted">{% trans "Get" %} Enterprise</a>
@@ -184,7 +184,7 @@ Blocks:
                 {% endif %}
                 {% endif %}
 
 
                 {# Commercial links #}
                 {# Commercial links #}
-                {% if settings.RELEASE.features.commercial %}
+                {% if settings.RELEASE.features.commercial and not settings.ISOLATED_DEPLOYMENT %}
                   {# LinkedIn #}
                   {# LinkedIn #}
                   <li class="list-inline-item">
                   <li class="list-inline-item">
                     <a href="https://www.linkedin.com/company/netboxlabs/" target="_blank" class="link-secondary" rel="noopener" aria-label="LinkedIn">
                     <a href="https://www.linkedin.com/company/netboxlabs/" target="_blank" class="link-secondary" rel="noopener" aria-label="LinkedIn">
@@ -199,7 +199,7 @@ Blocks:
                   </li>
                   </li>
 
 
                 {# Community links #}
                 {# Community links #}
-                {% else %}
+                {% elif not settings.ISOLATED_DEPLOYMENT %}
                   {# GitHub #}
                   {# GitHub #}
                   <li class="list-inline-item">
                   <li class="list-inline-item">
                     <a href="https://github.com/netbox-community/netbox" target="_blank" class="link-secondary" rel="noopener" aria-label="{% trans "Source Code" %}">
                     <a href="https://github.com/netbox-community/netbox" target="_blank" class="link-secondary" rel="noopener" aria-label="{% trans "Source Code" %}">

+ 1 - 1
netbox/templates/dcim/inc/rack_elevation.html

@@ -1,5 +1,5 @@
 {% load i18n %}
 {% load i18n %}
-<div style="margin-left: -30px">
+<div style="margin-left: -30px" class="rack_elevation">
   <div
   <div
     hx-get="{% url 'dcim-api:rack-elevation' pk=object.pk %}?face={{ face }}&render=svg{% if extra_params %}&{{ extra_params }}{% endif %}"
     hx-get="{% url 'dcim-api:rack-elevation' pk=object.pk %}?face={{ face }}&render=svg{% if extra_params %}&{{ extra_params }}{% endif %}"
     hx-trigger="intersect"
     hx-trigger="intersect"

+ 7 - 2
netbox/tenancy/forms/bulk_edit.py

@@ -45,12 +45,17 @@ class TenantBulkEditForm(NetBoxModelBulkEditForm):
         queryset=TenantGroup.objects.all(),
         queryset=TenantGroup.objects.all(),
         required=False
         required=False
     )
     )
+    description = forms.CharField(
+        label=_('Description'),
+        max_length=200,
+        required=False
+    )
 
 
     model = Tenant
     model = Tenant
     fieldsets = (
     fieldsets = (
-        FieldSet('group'),
+        FieldSet('group', 'description'),
     )
     )
-    nullable_fields = ('group',)
+    nullable_fields = ('group', 'description')
 
 
 
 
 #
 #

+ 1 - 0
netbox/tenancy/tests/test_views.py

@@ -98,6 +98,7 @@ class TenantTestCase(ViewTestCases.PrimaryObjectViewTestCase):
 
 
         cls.bulk_edit_data = {
         cls.bulk_edit_data = {
             'group': tenant_groups[1].pk,
             'group': tenant_groups[1].pk,
+            'description': 'Bulk edit description',
         }
         }
 
 
 
 

BIN
netbox/translations/cs/LC_MESSAGES/django.mo


File diff suppressed because it is too large
+ 208 - 172
netbox/translations/cs/LC_MESSAGES/django.po


BIN
netbox/translations/da/LC_MESSAGES/django.mo


File diff suppressed because it is too large
+ 206 - 170
netbox/translations/da/LC_MESSAGES/django.po


BIN
netbox/translations/de/LC_MESSAGES/django.mo


File diff suppressed because it is too large
+ 208 - 172
netbox/translations/de/LC_MESSAGES/django.po


+ 126 - 104
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-07-16 05:05+0000\n"
+"POT-Creation-Date: 2025-07-30 05:08+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"
@@ -215,8 +215,8 @@ msgstr ""
 #: netbox/dcim/forms/bulk_edit.py:344 netbox/dcim/forms/bulk_edit.py:730
 #: netbox/dcim/forms/bulk_edit.py:344 netbox/dcim/forms/bulk_edit.py:730
 #: netbox/dcim/forms/bulk_edit.py:935 netbox/dcim/forms/bulk_import.py:134
 #: netbox/dcim/forms/bulk_edit.py:935 netbox/dcim/forms/bulk_import.py:134
 #: netbox/dcim/forms/bulk_import.py:236 netbox/dcim/forms/bulk_import.py:337
 #: netbox/dcim/forms/bulk_import.py:236 netbox/dcim/forms/bulk_import.py:337
-#: netbox/dcim/forms/bulk_import.py:598 netbox/dcim/forms/bulk_import.py:1479
-#: netbox/dcim/forms/bulk_import.py:1507 netbox/dcim/forms/filtersets.py:89
+#: netbox/dcim/forms/bulk_import.py:598 netbox/dcim/forms/bulk_import.py:1539
+#: netbox/dcim/forms/bulk_import.py:1567 netbox/dcim/forms/filtersets.py:89
 #: netbox/dcim/forms/filtersets.py:227 netbox/dcim/forms/filtersets.py:344
 #: netbox/dcim/forms/filtersets.py:227 netbox/dcim/forms/filtersets.py:344
 #: netbox/dcim/forms/filtersets.py:441 netbox/dcim/forms/filtersets.py:773
 #: netbox/dcim/forms/filtersets.py:441 netbox/dcim/forms/filtersets.py:773
 #: netbox/dcim/forms/filtersets.py:992 netbox/dcim/forms/filtersets.py:1065
 #: netbox/dcim/forms/filtersets.py:992 netbox/dcim/forms/filtersets.py:1065
@@ -587,9 +587,10 @@ msgstr ""
 #: netbox/templates/wireless/wirelesslan.html:34
 #: netbox/templates/wireless/wirelesslan.html:34
 #: netbox/templates/wireless/wirelesslangroup.html:33
 #: netbox/templates/wireless/wirelesslangroup.html:33
 #: netbox/templates/wireless/wirelesslink.html:34
 #: netbox/templates/wireless/wirelesslink.html:34
-#: netbox/tenancy/forms/bulk_edit.py:32 netbox/tenancy/forms/bulk_edit.py:82
-#: netbox/tenancy/forms/bulk_edit.py:130 netbox/users/forms/bulk_edit.py:64
-#: netbox/users/forms/bulk_edit.py:82 netbox/users/forms/bulk_edit.py:112
+#: netbox/tenancy/forms/bulk_edit.py:32 netbox/tenancy/forms/bulk_edit.py:49
+#: netbox/tenancy/forms/bulk_edit.py:87 netbox/tenancy/forms/bulk_edit.py:135
+#: netbox/users/forms/bulk_edit.py:64 netbox/users/forms/bulk_edit.py:82
+#: netbox/users/forms/bulk_edit.py:112
 #: netbox/virtualization/forms/bulk_edit.py:33
 #: netbox/virtualization/forms/bulk_edit.py:33
 #: netbox/virtualization/forms/bulk_edit.py:47
 #: netbox/virtualization/forms/bulk_edit.py:47
 #: netbox/virtualization/forms/bulk_edit.py:82
 #: netbox/virtualization/forms/bulk_edit.py:82
@@ -655,13 +656,13 @@ msgstr ""
 #: netbox/circuits/forms/filtersets.py:321 netbox/dcim/forms/bulk_edit.py:216
 #: netbox/circuits/forms/filtersets.py:321 netbox/dcim/forms/bulk_edit.py:216
 #: netbox/dcim/forms/bulk_edit.py:656 netbox/dcim/forms/bulk_edit.py:866
 #: netbox/dcim/forms/bulk_edit.py:656 netbox/dcim/forms/bulk_edit.py:866
 #: netbox/dcim/forms/bulk_edit.py:1235 netbox/dcim/forms/bulk_edit.py:1262
 #: netbox/dcim/forms/bulk_edit.py:1235 netbox/dcim/forms/bulk_edit.py:1262
-#: netbox/dcim/forms/bulk_edit.py:1796 netbox/dcim/forms/filtersets.py:1132
-#: netbox/dcim/forms/filtersets.py:1390 netbox/dcim/forms/filtersets.py:1543
-#: netbox/dcim/forms/filtersets.py:1567 netbox/dcim/tables/devices.py:748
-#: netbox/dcim/tables/devices.py:804 netbox/dcim/tables/devices.py:1045
-#: netbox/dcim/tables/devicetypes.py:256 netbox/dcim/tables/devicetypes.py:271
-#: netbox/dcim/tables/racks.py:33 netbox/extras/forms/bulk_edit.py:303
-#: netbox/extras/tables/tables.py:487
+#: netbox/dcim/forms/bulk_edit.py:1796 netbox/dcim/forms/bulk_import.py:1414
+#: netbox/dcim/forms/filtersets.py:1132 netbox/dcim/forms/filtersets.py:1390
+#: netbox/dcim/forms/filtersets.py:1543 netbox/dcim/forms/filtersets.py:1567
+#: netbox/dcim/tables/devices.py:748 netbox/dcim/tables/devices.py:804
+#: netbox/dcim/tables/devices.py:1045 netbox/dcim/tables/devicetypes.py:256
+#: netbox/dcim/tables/devicetypes.py:271 netbox/dcim/tables/racks.py:33
+#: netbox/extras/forms/bulk_edit.py:303 netbox/extras/tables/tables.py:487
 #: netbox/templates/circuits/circuittype.html:30
 #: netbox/templates/circuits/circuittype.html:30
 #: netbox/templates/circuits/virtualcircuittype.html:30
 #: netbox/templates/circuits/virtualcircuittype.html:30
 #: netbox/templates/dcim/cable.html:40 netbox/templates/dcim/devicerole.html:38
 #: netbox/templates/dcim/cable.html:40 netbox/templates/dcim/devicerole.html:38
@@ -692,8 +693,8 @@ msgstr ""
 #: netbox/dcim/forms/bulk_import.py:766 netbox/dcim/forms/bulk_import.py:792
 #: netbox/dcim/forms/bulk_import.py:766 netbox/dcim/forms/bulk_import.py:792
 #: netbox/dcim/forms/bulk_import.py:818 netbox/dcim/forms/bulk_import.py:838
 #: netbox/dcim/forms/bulk_import.py:818 netbox/dcim/forms/bulk_import.py:838
 #: netbox/dcim/forms/bulk_import.py:924 netbox/dcim/forms/bulk_import.py:1018
 #: netbox/dcim/forms/bulk_import.py:924 netbox/dcim/forms/bulk_import.py:1018
-#: netbox/dcim/forms/bulk_import.py:1060 netbox/dcim/forms/bulk_import.py:1381
-#: netbox/dcim/forms/bulk_import.py:1544 netbox/dcim/forms/filtersets.py:1023
+#: netbox/dcim/forms/bulk_import.py:1060 netbox/dcim/forms/bulk_import.py:1395
+#: netbox/dcim/forms/bulk_import.py:1604 netbox/dcim/forms/filtersets.py:1023
 #: netbox/dcim/forms/filtersets.py:1122 netbox/dcim/forms/filtersets.py:1243
 #: netbox/dcim/forms/filtersets.py:1122 netbox/dcim/forms/filtersets.py:1243
 #: netbox/dcim/forms/filtersets.py:1315 netbox/dcim/forms/filtersets.py:1340
 #: netbox/dcim/forms/filtersets.py:1315 netbox/dcim/forms/filtersets.py:1340
 #: netbox/dcim/forms/filtersets.py:1364 netbox/dcim/forms/filtersets.py:1384
 #: netbox/dcim/forms/filtersets.py:1364 netbox/dcim/forms/filtersets.py:1384
@@ -763,8 +764,8 @@ msgstr ""
 #: netbox/dcim/forms/bulk_edit.py:1819 netbox/dcim/forms/bulk_import.py:91
 #: netbox/dcim/forms/bulk_edit.py:1819 netbox/dcim/forms/bulk_import.py:91
 #: netbox/dcim/forms/bulk_import.py:150 netbox/dcim/forms/bulk_import.py:254
 #: netbox/dcim/forms/bulk_import.py:150 netbox/dcim/forms/bulk_import.py:254
 #: netbox/dcim/forms/bulk_import.py:563 netbox/dcim/forms/bulk_import.py:717
 #: netbox/dcim/forms/bulk_import.py:563 netbox/dcim/forms/bulk_import.py:717
-#: netbox/dcim/forms/bulk_import.py:1168 netbox/dcim/forms/bulk_import.py:1375
-#: netbox/dcim/forms/bulk_import.py:1539 netbox/dcim/forms/bulk_import.py:1603
+#: netbox/dcim/forms/bulk_import.py:1168 netbox/dcim/forms/bulk_import.py:1389
+#: netbox/dcim/forms/bulk_import.py:1599 netbox/dcim/forms/bulk_import.py:1663
 #: netbox/dcim/forms/filtersets.py:180 netbox/dcim/forms/filtersets.py:239
 #: netbox/dcim/forms/filtersets.py:180 netbox/dcim/forms/filtersets.py:239
 #: netbox/dcim/forms/filtersets.py:361 netbox/dcim/forms/filtersets.py:819
 #: netbox/dcim/forms/filtersets.py:361 netbox/dcim/forms/filtersets.py:819
 #: netbox/dcim/forms/filtersets.py:944 netbox/dcim/forms/filtersets.py:1026
 #: netbox/dcim/forms/filtersets.py:944 netbox/dcim/forms/filtersets.py:1026
@@ -841,8 +842,8 @@ msgstr ""
 #: netbox/dcim/forms/bulk_edit.py:856 netbox/dcim/forms/bulk_edit.py:1824
 #: netbox/dcim/forms/bulk_edit.py:856 netbox/dcim/forms/bulk_edit.py:1824
 #: netbox/dcim/forms/bulk_import.py:110 netbox/dcim/forms/bulk_import.py:155
 #: netbox/dcim/forms/bulk_import.py:110 netbox/dcim/forms/bulk_import.py:155
 #: netbox/dcim/forms/bulk_import.py:247 netbox/dcim/forms/bulk_import.py:362
 #: netbox/dcim/forms/bulk_import.py:247 netbox/dcim/forms/bulk_import.py:362
-#: netbox/dcim/forms/bulk_import.py:537 netbox/dcim/forms/bulk_import.py:1387
-#: netbox/dcim/forms/bulk_import.py:1596 netbox/dcim/forms/filtersets.py:175
+#: netbox/dcim/forms/bulk_import.py:537 netbox/dcim/forms/bulk_import.py:1401
+#: netbox/dcim/forms/bulk_import.py:1656 netbox/dcim/forms/filtersets.py:175
 #: netbox/dcim/forms/filtersets.py:207 netbox/dcim/forms/filtersets.py:325
 #: netbox/dcim/forms/filtersets.py:207 netbox/dcim/forms/filtersets.py:325
 #: netbox/dcim/forms/filtersets.py:401 netbox/dcim/forms/filtersets.py:422
 #: netbox/dcim/forms/filtersets.py:401 netbox/dcim/forms/filtersets.py:422
 #: netbox/dcim/forms/filtersets.py:742 netbox/dcim/forms/filtersets.py:936
 #: netbox/dcim/forms/filtersets.py:742 netbox/dcim/forms/filtersets.py:936
@@ -1012,7 +1013,7 @@ msgstr ""
 
 
 #: netbox/circuits/forms/bulk_edit.py:215
 #: netbox/circuits/forms/bulk_edit.py:215
 #: netbox/circuits/forms/model_forms.py:171
 #: netbox/circuits/forms/model_forms.py:171
-#: netbox/dcim/forms/bulk_import.py:1348 netbox/dcim/forms/bulk_import.py:1366
+#: netbox/dcim/forms/bulk_import.py:1355 netbox/dcim/forms/bulk_import.py:1380
 msgid "Termination type"
 msgid "Termination type"
 msgstr ""
 msgstr ""
 
 
@@ -1065,7 +1066,7 @@ msgstr ""
 #: netbox/templates/dcim/virtualchassis.html:68
 #: netbox/templates/dcim/virtualchassis.html:68
 #: netbox/templates/dcim/virtualchassis_edit.html:60
 #: netbox/templates/dcim/virtualchassis_edit.html:60
 #: netbox/templates/ipam/inc/panels/fhrp_groups.html:26
 #: netbox/templates/ipam/inc/panels/fhrp_groups.html:26
-#: netbox/tenancy/forms/bulk_edit.py:159 netbox/tenancy/forms/filtersets.py:110
+#: netbox/tenancy/forms/bulk_edit.py:164 netbox/tenancy/forms/filtersets.py:110
 msgid "Priority"
 msgid "Priority"
 msgstr ""
 msgstr ""
 
 
@@ -1116,7 +1117,7 @@ msgstr ""
 #: netbox/templates/virtualization/virtualmachine.html:23
 #: netbox/templates/virtualization/virtualmachine.html:23
 #: netbox/templates/vpn/tunneltermination.html:17
 #: netbox/templates/vpn/tunneltermination.html:17
 #: netbox/templates/wireless/inc/wirelesslink_interface.html:20
 #: netbox/templates/wireless/inc/wirelesslink_interface.html:20
-#: netbox/tenancy/forms/bulk_edit.py:154 netbox/tenancy/forms/filtersets.py:107
+#: netbox/tenancy/forms/bulk_edit.py:159 netbox/tenancy/forms/filtersets.py:107
 #: netbox/tenancy/forms/model_forms.py:139
 #: netbox/tenancy/forms/model_forms.py:139
 #: netbox/tenancy/tables/contacts.py:110
 #: netbox/tenancy/tables/contacts.py:110
 #: netbox/virtualization/forms/bulk_edit.py:127
 #: netbox/virtualization/forms/bulk_edit.py:127
@@ -1148,7 +1149,7 @@ msgstr ""
 #: netbox/circuits/forms/bulk_import.py:229 netbox/dcim/forms/bulk_import.py:93
 #: netbox/circuits/forms/bulk_import.py:229 netbox/dcim/forms/bulk_import.py:93
 #: netbox/dcim/forms/bulk_import.py:152 netbox/dcim/forms/bulk_import.py:256
 #: netbox/dcim/forms/bulk_import.py:152 netbox/dcim/forms/bulk_import.py:256
 #: netbox/dcim/forms/bulk_import.py:565 netbox/dcim/forms/bulk_import.py:719
 #: netbox/dcim/forms/bulk_import.py:565 netbox/dcim/forms/bulk_import.py:719
-#: netbox/dcim/forms/bulk_import.py:1170 netbox/dcim/forms/bulk_import.py:1541
+#: netbox/dcim/forms/bulk_import.py:1170 netbox/dcim/forms/bulk_import.py:1601
 #: netbox/ipam/forms/bulk_import.py:197 netbox/ipam/forms/bulk_import.py:265
 #: netbox/ipam/forms/bulk_import.py:197 netbox/ipam/forms/bulk_import.py:265
 #: netbox/ipam/forms/bulk_import.py:301 netbox/ipam/forms/bulk_import.py:498
 #: netbox/ipam/forms/bulk_import.py:301 netbox/ipam/forms/bulk_import.py:498
 #: netbox/ipam/forms/bulk_import.py:511
 #: netbox/ipam/forms/bulk_import.py:511
@@ -1164,8 +1165,8 @@ msgstr ""
 #: netbox/circuits/forms/bulk_import.py:236
 #: netbox/circuits/forms/bulk_import.py:236
 #: netbox/dcim/forms/bulk_import.py:114 netbox/dcim/forms/bulk_import.py:159
 #: netbox/dcim/forms/bulk_import.py:114 netbox/dcim/forms/bulk_import.py:159
 #: netbox/dcim/forms/bulk_import.py:366 netbox/dcim/forms/bulk_import.py:541
 #: netbox/dcim/forms/bulk_import.py:366 netbox/dcim/forms/bulk_import.py:541
-#: netbox/dcim/forms/bulk_import.py:1391 netbox/dcim/forms/bulk_import.py:1536
-#: netbox/dcim/forms/bulk_import.py:1600 netbox/ipam/forms/bulk_import.py:45
+#: netbox/dcim/forms/bulk_import.py:1405 netbox/dcim/forms/bulk_import.py:1596
+#: netbox/dcim/forms/bulk_import.py:1660 netbox/ipam/forms/bulk_import.py:45
 #: netbox/ipam/forms/bulk_import.py:74 netbox/ipam/forms/bulk_import.py:102
 #: netbox/ipam/forms/bulk_import.py:74 netbox/ipam/forms/bulk_import.py:102
 #: netbox/ipam/forms/bulk_import.py:122 netbox/ipam/forms/bulk_import.py:142
 #: netbox/ipam/forms/bulk_import.py:122 netbox/ipam/forms/bulk_import.py:142
 #: netbox/ipam/forms/bulk_import.py:171 netbox/ipam/forms/bulk_import.py:260
 #: netbox/ipam/forms/bulk_import.py:171 netbox/ipam/forms/bulk_import.py:260
@@ -1245,8 +1246,8 @@ msgstr ""
 #: netbox/dcim/forms/bulk_edit.py:466 netbox/dcim/forms/bulk_edit.py:735
 #: netbox/dcim/forms/bulk_edit.py:466 netbox/dcim/forms/bulk_edit.py:735
 #: netbox/dcim/forms/bulk_edit.py:790 netbox/dcim/forms/bulk_edit.py:944
 #: netbox/dcim/forms/bulk_edit.py:790 netbox/dcim/forms/bulk_edit.py:944
 #: netbox/dcim/forms/bulk_import.py:241 netbox/dcim/forms/bulk_import.py:343
 #: netbox/dcim/forms/bulk_import.py:241 netbox/dcim/forms/bulk_import.py:343
-#: netbox/dcim/forms/bulk_import.py:604 netbox/dcim/forms/bulk_import.py:1485
-#: netbox/dcim/forms/bulk_import.py:1519 netbox/dcim/forms/filtersets.py:97
+#: netbox/dcim/forms/bulk_import.py:604 netbox/dcim/forms/bulk_import.py:1545
+#: netbox/dcim/forms/bulk_import.py:1579 netbox/dcim/forms/filtersets.py:97
 #: netbox/dcim/forms/filtersets.py:324 netbox/dcim/forms/filtersets.py:358
 #: netbox/dcim/forms/filtersets.py:324 netbox/dcim/forms/filtersets.py:358
 #: netbox/dcim/forms/filtersets.py:398 netbox/dcim/forms/filtersets.py:449
 #: netbox/dcim/forms/filtersets.py:398 netbox/dcim/forms/filtersets.py:449
 #: netbox/dcim/forms/filtersets.py:739 netbox/dcim/forms/filtersets.py:782
 #: netbox/dcim/forms/filtersets.py:739 netbox/dcim/forms/filtersets.py:782
@@ -1948,7 +1949,7 @@ msgstr ""
 #: netbox/dcim/forms/bulk_import.py:1007 netbox/dcim/forms/bulk_import.py:1055
 #: netbox/dcim/forms/bulk_import.py:1007 netbox/dcim/forms/bulk_import.py:1055
 #: netbox/dcim/forms/bulk_import.py:1072 netbox/dcim/forms/bulk_import.py:1084
 #: netbox/dcim/forms/bulk_import.py:1072 netbox/dcim/forms/bulk_import.py:1084
 #: netbox/dcim/forms/bulk_import.py:1132 netbox/dcim/forms/bulk_import.py:1254
 #: netbox/dcim/forms/bulk_import.py:1132 netbox/dcim/forms/bulk_import.py:1254
-#: netbox/dcim/forms/bulk_import.py:1590 netbox/dcim/forms/connections.py:24
+#: netbox/dcim/forms/bulk_import.py:1650 netbox/dcim/forms/connections.py:24
 #: netbox/dcim/forms/filtersets.py:133 netbox/dcim/forms/filtersets.py:941
 #: netbox/dcim/forms/filtersets.py:133 netbox/dcim/forms/filtersets.py:941
 #: netbox/dcim/forms/filtersets.py:973 netbox/dcim/forms/filtersets.py:1119
 #: netbox/dcim/forms/filtersets.py:973 netbox/dcim/forms/filtersets.py:1119
 #: netbox/dcim/forms/filtersets.py:1310 netbox/dcim/forms/filtersets.py:1335
 #: netbox/dcim/forms/filtersets.py:1310 netbox/dcim/forms/filtersets.py:1335
@@ -2017,7 +2018,7 @@ msgstr ""
 msgid "Swapped terminations for circuit {circuit}."
 msgid "Swapped terminations for circuit {circuit}."
 msgstr ""
 msgstr ""
 
 
-#: netbox/core/api/views.py:51
+#: netbox/core/api/views.py:50
 msgid "This user does not have permission to synchronize this data source."
 msgid "This user does not have permission to synchronize this data source."
 msgstr ""
 msgstr ""
 
 
@@ -2748,7 +2749,7 @@ msgid ""
 "enqueue() cannot be called with values for both schedule_at and immediate."
 "enqueue() cannot be called with values for both schedule_at and immediate."
 msgstr ""
 msgstr ""
 
 
-#: netbox/core/signals.py:125
+#: netbox/core/signals.py:131
 #, python-brace-format
 #, python-brace-format
 msgid "Deletion is prevented by a protection rule: {message}"
 msgid "Deletion is prevented by a protection rule: {message}"
 msgstr ""
 msgstr ""
@@ -2913,7 +2914,7 @@ msgstr ""
 msgid "No workers found"
 msgid "No workers found"
 msgstr ""
 msgstr ""
 
 
-#: netbox/core/utils.py:84 netbox/core/utils.py:150 netbox/core/views.py:398
+#: netbox/core/utils.py:84 netbox/core/utils.py:150 netbox/core/views.py:393
 #, python-brace-format
 #, python-brace-format
 msgid "Job {job_id} not found"
 msgid "Job {job_id} not found"
 msgstr ""
 msgstr ""
@@ -2923,51 +2924,51 @@ msgstr ""
 msgid "Job {id} not found."
 msgid "Job {id} not found."
 msgstr ""
 msgstr ""
 
 
-#: netbox/core/views.py:89
+#: netbox/core/views.py:84
 #, python-brace-format
 #, python-brace-format
 msgid "Queued job #{id} to sync {datasource}"
 msgid "Queued job #{id} to sync {datasource}"
 msgstr ""
 msgstr ""
 
 
-#: netbox/core/views.py:334
+#: netbox/core/views.py:329
 #, python-brace-format
 #, python-brace-format
 msgid "Restored configuration revision #{id}"
 msgid "Restored configuration revision #{id}"
 msgstr ""
 msgstr ""
 
 
-#: netbox/core/views.py:437
+#: netbox/core/views.py:432
 #, python-brace-format
 #, python-brace-format
 msgid "Job {id} has been deleted."
 msgid "Job {id} has been deleted."
 msgstr ""
 msgstr ""
 
 
-#: netbox/core/views.py:439
+#: netbox/core/views.py:434
 #, python-brace-format
 #, python-brace-format
 msgid "Error deleting job {id}: {error}"
 msgid "Error deleting job {id}: {error}"
 msgstr ""
 msgstr ""
 
 
-#: netbox/core/views.py:448
+#: netbox/core/views.py:443
 #, python-brace-format
 #, python-brace-format
 msgid "Job {id} has been re-enqueued."
 msgid "Job {id} has been re-enqueued."
 msgstr ""
 msgstr ""
 
 
-#: netbox/core/views.py:457
+#: netbox/core/views.py:452
 #, python-brace-format
 #, python-brace-format
 msgid "Job {id} has been enqueued."
 msgid "Job {id} has been enqueued."
 msgstr ""
 msgstr ""
 
 
-#: netbox/core/views.py:466
+#: netbox/core/views.py:461
 #, python-brace-format
 #, python-brace-format
 msgid "Job {id} has been stopped."
 msgid "Job {id} has been stopped."
 msgstr ""
 msgstr ""
 
 
-#: netbox/core/views.py:468
+#: netbox/core/views.py:463
 #, python-brace-format
 #, python-brace-format
 msgid "Failed to stop job {id}"
 msgid "Failed to stop job {id}"
 msgstr ""
 msgstr ""
 
 
-#: netbox/core/views.py:603
+#: netbox/core/views.py:598
 msgid "Plugins catalog could not be loaded"
 msgid "Plugins catalog could not be loaded"
 msgstr ""
 msgstr ""
 
 
-#: netbox/core/views.py:639
+#: netbox/core/views.py:634
 #, python-brace-format
 #, python-brace-format
 msgid "Plugin {name} not found"
 msgid "Plugin {name} not found"
 msgstr ""
 msgstr ""
@@ -3106,7 +3107,7 @@ msgstr ""
 #: netbox/templates/tenancy/tenantgroup.html:37
 #: netbox/templates/tenancy/tenantgroup.html:37
 #: netbox/templates/virtualization/vminterface.html:39
 #: netbox/templates/virtualization/vminterface.html:39
 #: netbox/templates/wireless/wirelesslangroup.html:37
 #: netbox/templates/wireless/wirelesslangroup.html:37
-#: netbox/tenancy/forms/bulk_edit.py:27 netbox/tenancy/forms/bulk_edit.py:62
+#: netbox/tenancy/forms/bulk_edit.py:27 netbox/tenancy/forms/bulk_edit.py:67
 #: netbox/tenancy/forms/bulk_import.py:24
 #: netbox/tenancy/forms/bulk_import.py:24
 #: netbox/tenancy/forms/bulk_import.py:58
 #: netbox/tenancy/forms/bulk_import.py:58
 #: netbox/tenancy/forms/model_forms.py:25
 #: netbox/tenancy/forms/model_forms.py:25
@@ -4193,8 +4194,8 @@ msgstr ""
 
 
 #: netbox/dcim/forms/bulk_edit.py:465 netbox/dcim/forms/bulk_edit.py:972
 #: netbox/dcim/forms/bulk_edit.py:465 netbox/dcim/forms/bulk_edit.py:972
 #: netbox/dcim/forms/bulk_import.py:350 netbox/dcim/forms/bulk_import.py:353
 #: netbox/dcim/forms/bulk_import.py:350 netbox/dcim/forms/bulk_import.py:353
-#: netbox/dcim/forms/bulk_import.py:611 netbox/dcim/forms/bulk_import.py:1526
-#: netbox/dcim/forms/bulk_import.py:1530 netbox/dcim/forms/filtersets.py:106
+#: netbox/dcim/forms/bulk_import.py:611 netbox/dcim/forms/bulk_import.py:1586
+#: netbox/dcim/forms/bulk_import.py:1590 netbox/dcim/forms/filtersets.py:106
 #: netbox/dcim/forms/filtersets.py:326 netbox/dcim/forms/filtersets.py:407
 #: netbox/dcim/forms/filtersets.py:326 netbox/dcim/forms/filtersets.py:407
 #: netbox/dcim/forms/filtersets.py:421 netbox/dcim/forms/filtersets.py:459
 #: netbox/dcim/forms/filtersets.py:421 netbox/dcim/forms/filtersets.py:459
 #: netbox/dcim/forms/filtersets.py:792 netbox/dcim/forms/filtersets.py:1005
 #: netbox/dcim/forms/filtersets.py:792 netbox/dcim/forms/filtersets.py:1005
@@ -4394,8 +4395,8 @@ msgstr ""
 msgid "Length"
 msgid "Length"
 msgstr ""
 msgstr ""
 
 
-#: netbox/dcim/forms/bulk_edit.py:875 netbox/dcim/forms/bulk_import.py:1394
-#: netbox/dcim/forms/bulk_import.py:1397 netbox/dcim/forms/filtersets.py:1140
+#: netbox/dcim/forms/bulk_edit.py:875 netbox/dcim/forms/bulk_import.py:1408
+#: netbox/dcim/forms/bulk_import.py:1411 netbox/dcim/forms/filtersets.py:1140
 msgid "Length unit"
 msgid "Length unit"
 msgstr ""
 msgstr ""
 
 
@@ -4404,17 +4405,17 @@ msgstr ""
 msgid "Domain"
 msgid "Domain"
 msgstr ""
 msgstr ""
 
 
-#: netbox/dcim/forms/bulk_edit.py:967 netbox/dcim/forms/bulk_import.py:1513
+#: netbox/dcim/forms/bulk_edit.py:967 netbox/dcim/forms/bulk_import.py:1573
 #: netbox/dcim/forms/filtersets.py:1226 netbox/dcim/forms/model_forms.py:855
 #: netbox/dcim/forms/filtersets.py:1226 netbox/dcim/forms/model_forms.py:855
 msgid "Power panel"
 msgid "Power panel"
 msgstr ""
 msgstr ""
 
 
-#: netbox/dcim/forms/bulk_edit.py:989 netbox/dcim/forms/bulk_import.py:1549
+#: netbox/dcim/forms/bulk_edit.py:989 netbox/dcim/forms/bulk_import.py:1609
 #: netbox/dcim/forms/filtersets.py:1248 netbox/templates/dcim/powerfeed.html:83
 #: netbox/dcim/forms/filtersets.py:1248 netbox/templates/dcim/powerfeed.html:83
 msgid "Supply"
 msgid "Supply"
 msgstr ""
 msgstr ""
 
 
-#: netbox/dcim/forms/bulk_edit.py:995 netbox/dcim/forms/bulk_import.py:1554
+#: netbox/dcim/forms/bulk_edit.py:995 netbox/dcim/forms/bulk_import.py:1614
 #: netbox/dcim/forms/filtersets.py:1253 netbox/templates/dcim/powerfeed.html:95
 #: netbox/dcim/forms/filtersets.py:1253 netbox/templates/dcim/powerfeed.html:95
 msgid "Phase"
 msgid "Phase"
 msgstr ""
 msgstr ""
@@ -4652,7 +4653,7 @@ msgid "available options"
 msgstr ""
 msgstr ""
 
 
 #: netbox/dcim/forms/bulk_import.py:137 netbox/dcim/forms/bulk_import.py:601
 #: netbox/dcim/forms/bulk_import.py:137 netbox/dcim/forms/bulk_import.py:601
-#: netbox/dcim/forms/bulk_import.py:1510 netbox/ipam/forms/bulk_import.py:479
+#: netbox/dcim/forms/bulk_import.py:1570 netbox/ipam/forms/bulk_import.py:479
 #: netbox/virtualization/forms/bulk_import.py:64
 #: netbox/virtualization/forms/bulk_import.py:64
 #: netbox/virtualization/forms/bulk_import.py:95
 #: netbox/virtualization/forms/bulk_import.py:95
 msgid "Assigned site"
 msgid "Assigned site"
@@ -4715,7 +4716,7 @@ msgstr ""
 msgid "Parent site"
 msgid "Parent site"
 msgstr ""
 msgstr ""
 
 
-#: netbox/dcim/forms/bulk_import.py:347 netbox/dcim/forms/bulk_import.py:1523
+#: netbox/dcim/forms/bulk_import.py:347 netbox/dcim/forms/bulk_import.py:1583
 msgid "Rack's location (if any)"
 msgid "Rack's location (if any)"
 msgstr ""
 msgstr ""
 
 
@@ -4766,7 +4767,7 @@ msgstr ""
 msgid "Limit platform assignments to this manufacturer"
 msgid "Limit platform assignments to this manufacturer"
 msgstr ""
 msgstr ""
 
 
-#: netbox/dcim/forms/bulk_import.py:534 netbox/dcim/forms/bulk_import.py:1593
+#: netbox/dcim/forms/bulk_import.py:534 netbox/dcim/forms/bulk_import.py:1653
 #: netbox/tenancy/forms/bulk_import.py:105
 #: netbox/tenancy/forms/bulk_import.py:105
 msgid "Assigned role"
 msgid "Assigned role"
 msgstr ""
 msgstr ""
@@ -4956,7 +4957,7 @@ msgid "Corresponding rear port"
 msgstr ""
 msgstr ""
 
 
 #: netbox/dcim/forms/bulk_import.py:1020 netbox/dcim/forms/bulk_import.py:1061
 #: netbox/dcim/forms/bulk_import.py:1020 netbox/dcim/forms/bulk_import.py:1061
-#: netbox/dcim/forms/bulk_import.py:1384
+#: netbox/dcim/forms/bulk_import.py:1398
 msgid "Physical medium classification"
 msgid "Physical medium classification"
 msgstr ""
 msgstr ""
 
 
@@ -5045,102 +5046,131 @@ msgid "Must specify the parent device or VM when assigning an interface"
 msgstr ""
 msgstr ""
 
 
 #: netbox/dcim/forms/bulk_import.py:1339
 #: netbox/dcim/forms/bulk_import.py:1339
+msgid "Side A site"
+msgstr ""
+
+#: netbox/dcim/forms/bulk_import.py:1343
+#: netbox/wireless/forms/bulk_import.py:94
+msgid "Site of parent device A (if any)"
+msgstr ""
+
+#: netbox/dcim/forms/bulk_import.py:1346
 msgid "Side A device"
 msgid "Side A device"
 msgstr ""
 msgstr ""
 
 
-#: netbox/dcim/forms/bulk_import.py:1342 netbox/dcim/forms/bulk_import.py:1360
+#: netbox/dcim/forms/bulk_import.py:1349 netbox/dcim/forms/bulk_import.py:1374
 msgid "Device name"
 msgid "Device name"
 msgstr ""
 msgstr ""
 
 
-#: netbox/dcim/forms/bulk_import.py:1345
+#: netbox/dcim/forms/bulk_import.py:1352
 msgid "Side A type"
 msgid "Side A type"
 msgstr ""
 msgstr ""
 
 
-#: netbox/dcim/forms/bulk_import.py:1351
+#: netbox/dcim/forms/bulk_import.py:1358
 msgid "Side A name"
 msgid "Side A name"
 msgstr ""
 msgstr ""
 
 
-#: netbox/dcim/forms/bulk_import.py:1352 netbox/dcim/forms/bulk_import.py:1370
+#: netbox/dcim/forms/bulk_import.py:1359 netbox/dcim/forms/bulk_import.py:1384
 msgid "Termination name"
 msgid "Termination name"
 msgstr ""
 msgstr ""
 
 
-#: netbox/dcim/forms/bulk_import.py:1357
+#: netbox/dcim/forms/bulk_import.py:1364
+msgid "Side B site"
+msgstr ""
+
+#: netbox/dcim/forms/bulk_import.py:1368
+#: netbox/wireless/forms/bulk_import.py:115
+msgid "Site of parent device B (if any)"
+msgstr ""
+
+#: netbox/dcim/forms/bulk_import.py:1371
 msgid "Side B device"
 msgid "Side B device"
 msgstr ""
 msgstr ""
 
 
-#: netbox/dcim/forms/bulk_import.py:1363
+#: netbox/dcim/forms/bulk_import.py:1377
 msgid "Side B type"
 msgid "Side B type"
 msgstr ""
 msgstr ""
 
 
-#: netbox/dcim/forms/bulk_import.py:1369
+#: netbox/dcim/forms/bulk_import.py:1383
 msgid "Side B name"
 msgid "Side B name"
 msgstr ""
 msgstr ""
 
 
-#: netbox/dcim/forms/bulk_import.py:1378
+#: netbox/dcim/forms/bulk_import.py:1392
 #: netbox/wireless/forms/bulk_import.py:134
 #: netbox/wireless/forms/bulk_import.py:134
 msgid "Connection status"
 msgid "Connection status"
 msgstr ""
 msgstr ""
 
 
-#: netbox/dcim/forms/bulk_import.py:1430
+#: netbox/dcim/forms/bulk_import.py:1417
+msgid "Color name (e.g. \"Red\") or hex code (e.g. \"f44336\")"
+msgstr ""
+
+#: netbox/dcim/forms/bulk_import.py:1469
 #, python-brace-format
 #, python-brace-format
 msgid "Side {side_upper}: {device} {termination_object} is already connected"
 msgid "Side {side_upper}: {device} {termination_object} is already connected"
 msgstr ""
 msgstr ""
 
 
-#: netbox/dcim/forms/bulk_import.py:1436
+#: netbox/dcim/forms/bulk_import.py:1475
 #, python-brace-format
 #, python-brace-format
 msgid "{side_upper} side termination not found: {device} {name}"
 msgid "{side_upper} side termination not found: {device} {name}"
 msgstr ""
 msgstr ""
 
 
-#: netbox/dcim/forms/bulk_import.py:1461 netbox/dcim/forms/model_forms.py:891
+#: netbox/dcim/forms/bulk_import.py:1496
+#, python-brace-format
+msgid ""
+"{color} did not match any used color name and was longer than six "
+"characters: invalid hex."
+msgstr ""
+
+#: netbox/dcim/forms/bulk_import.py:1521 netbox/dcim/forms/model_forms.py:891
 #: netbox/dcim/tables/devices.py:1069 netbox/templates/dcim/device.html:138
 #: netbox/dcim/tables/devices.py:1069 netbox/templates/dcim/device.html:138
 #: netbox/templates/dcim/virtualchassis.html:27
 #: netbox/templates/dcim/virtualchassis.html:27
 #: netbox/templates/dcim/virtualchassis.html:67
 #: netbox/templates/dcim/virtualchassis.html:67
 msgid "Master"
 msgid "Master"
 msgstr ""
 msgstr ""
 
 
-#: netbox/dcim/forms/bulk_import.py:1465
+#: netbox/dcim/forms/bulk_import.py:1525
 msgid "Master device"
 msgid "Master device"
 msgstr ""
 msgstr ""
 
 
-#: netbox/dcim/forms/bulk_import.py:1482
+#: netbox/dcim/forms/bulk_import.py:1542
 msgid "Name of parent site"
 msgid "Name of parent site"
 msgstr ""
 msgstr ""
 
 
-#: netbox/dcim/forms/bulk_import.py:1516
+#: netbox/dcim/forms/bulk_import.py:1576
 msgid "Upstream power panel"
 msgid "Upstream power panel"
 msgstr ""
 msgstr ""
 
 
-#: netbox/dcim/forms/bulk_import.py:1546
+#: netbox/dcim/forms/bulk_import.py:1606
 msgid "Primary or redundant"
 msgid "Primary or redundant"
 msgstr ""
 msgstr ""
 
 
-#: netbox/dcim/forms/bulk_import.py:1551
+#: netbox/dcim/forms/bulk_import.py:1611
 msgid "Supply type (AC/DC)"
 msgid "Supply type (AC/DC)"
 msgstr ""
 msgstr ""
 
 
-#: netbox/dcim/forms/bulk_import.py:1556
+#: netbox/dcim/forms/bulk_import.py:1616
 msgid "Single or three-phase"
 msgid "Single or three-phase"
 msgstr ""
 msgstr ""
 
 
-#: netbox/dcim/forms/bulk_import.py:1607 netbox/dcim/forms/model_forms.py:1847
+#: netbox/dcim/forms/bulk_import.py:1667 netbox/dcim/forms/model_forms.py:1847
 #: netbox/templates/dcim/device.html:196
 #: netbox/templates/dcim/device.html:196
 #: netbox/templates/dcim/virtualdevicecontext.html:30
 #: netbox/templates/dcim/virtualdevicecontext.html:30
 #: netbox/templates/virtualization/virtualmachine.html:52
 #: netbox/templates/virtualization/virtualmachine.html:52
 msgid "Primary IPv4"
 msgid "Primary IPv4"
 msgstr ""
 msgstr ""
 
 
-#: netbox/dcim/forms/bulk_import.py:1611
+#: netbox/dcim/forms/bulk_import.py:1671
 msgid "IPv4 address with mask, e.g. 1.2.3.4/24"
 msgid "IPv4 address with mask, e.g. 1.2.3.4/24"
 msgstr ""
 msgstr ""
 
 
-#: netbox/dcim/forms/bulk_import.py:1614 netbox/dcim/forms/model_forms.py:1856
+#: netbox/dcim/forms/bulk_import.py:1674 netbox/dcim/forms/model_forms.py:1856
 #: netbox/templates/dcim/device.html:212
 #: netbox/templates/dcim/device.html:212
 #: netbox/templates/dcim/virtualdevicecontext.html:41
 #: netbox/templates/dcim/virtualdevicecontext.html:41
 #: netbox/templates/virtualization/virtualmachine.html:68
 #: netbox/templates/virtualization/virtualmachine.html:68
 msgid "Primary IPv6"
 msgid "Primary IPv6"
 msgstr ""
 msgstr ""
 
 
-#: netbox/dcim/forms/bulk_import.py:1618
+#: netbox/dcim/forms/bulk_import.py:1678
 msgid "IPv6 address with prefix length, e.g. 2001:db8::1/64"
 msgid "IPv6 address with prefix length, e.g. 2001:db8::1/64"
 msgstr ""
 msgstr ""
 
 
@@ -7784,7 +7814,7 @@ msgid "No"
 msgstr ""
 msgstr ""
 
 
 #: netbox/extras/choices.py:108 netbox/templates/tenancy/contact.html:67
 #: netbox/extras/choices.py:108 netbox/templates/tenancy/contact.html:67
-#: netbox/tenancy/forms/bulk_edit.py:125
+#: netbox/tenancy/forms/bulk_edit.py:130
 #: netbox/wireless/forms/model_forms.py:173
 #: netbox/wireless/forms/model_forms.py:173
 msgid "Link"
 msgid "Link"
 msgstr ""
 msgstr ""
@@ -8146,7 +8176,7 @@ msgstr ""
 
 
 #: netbox/extras/forms/bulk_edit.py:155 netbox/extras/forms/bulk_edit.py:354
 #: netbox/extras/forms/bulk_edit.py:155 netbox/extras/forms/bulk_edit.py:354
 #: netbox/extras/forms/filtersets.py:192 netbox/extras/forms/filtersets.py:470
 #: netbox/extras/forms/filtersets.py:192 netbox/extras/forms/filtersets.py:470
-#: netbox/extras/models/mixins.py:100
+#: netbox/extras/models/mixins.py:101
 msgid "MIME type"
 msgid "MIME type"
 msgstr ""
 msgstr ""
 
 
@@ -8325,7 +8355,7 @@ msgstr ""
 #: netbox/netbox/navigation/menu.py:433
 #: netbox/netbox/navigation/menu.py:433
 #: netbox/templates/extras/notificationgroup.html:31
 #: netbox/templates/extras/notificationgroup.html:31
 #: netbox/templates/tenancy/contact.html:21
 #: netbox/templates/tenancy/contact.html:21
-#: netbox/tenancy/forms/bulk_edit.py:139 netbox/tenancy/forms/filtersets.py:78
+#: netbox/tenancy/forms/bulk_edit.py:144 netbox/tenancy/forms/filtersets.py:78
 #: netbox/tenancy/forms/model_forms.py:99 netbox/tenancy/tables/contacts.py:68
 #: netbox/tenancy/forms/model_forms.py:99 netbox/tenancy/tables/contacts.py:68
 #: netbox/users/forms/model_forms.py:182 netbox/users/forms/model_forms.py:194
 #: netbox/users/forms/model_forms.py:182 netbox/users/forms/model_forms.py:194
 #: netbox/users/forms/model_forms.py:306 netbox/users/tables.py:35
 #: netbox/users/forms/model_forms.py:306 netbox/users/tables.py:35
@@ -9025,51 +9055,51 @@ msgstr ""
 msgid "dashboards"
 msgid "dashboards"
 msgstr ""
 msgstr ""
 
 
-#: netbox/extras/models/mixins.py:85
+#: netbox/extras/models/mixins.py:86
 msgid "template code"
 msgid "template code"
 msgstr ""
 msgstr ""
 
 
-#: netbox/extras/models/mixins.py:86
+#: netbox/extras/models/mixins.py:87
 msgid "Jinja template code."
 msgid "Jinja template code."
 msgstr ""
 msgstr ""
 
 
-#: netbox/extras/models/mixins.py:89
+#: netbox/extras/models/mixins.py:90
 msgid "environment parameters"
 msgid "environment parameters"
 msgstr ""
 msgstr ""
 
 
-#: netbox/extras/models/mixins.py:94
+#: netbox/extras/models/mixins.py:95
 #, python-brace-format
 #, python-brace-format
 msgid ""
 msgid ""
 "Any <a href=\"{url}\">additional parameters</a> to pass when constructing "
 "Any <a href=\"{url}\">additional parameters</a> to pass when constructing "
 "the Jinja environment"
 "the Jinja environment"
 msgstr ""
 msgstr ""
 
 
-#: netbox/extras/models/mixins.py:101
+#: netbox/extras/models/mixins.py:102
 #, python-brace-format
 #, python-brace-format
 msgid "Defaults to <code>{default}</code>"
 msgid "Defaults to <code>{default}</code>"
 msgstr ""
 msgstr ""
 
 
-#: netbox/extras/models/mixins.py:106
+#: netbox/extras/models/mixins.py:107
 msgid "Filename to give to the rendered export file"
 msgid "Filename to give to the rendered export file"
 msgstr ""
 msgstr ""
 
 
-#: netbox/extras/models/mixins.py:109
+#: netbox/extras/models/mixins.py:110
 msgid "file extension"
 msgid "file extension"
 msgstr ""
 msgstr ""
 
 
-#: netbox/extras/models/mixins.py:112
+#: netbox/extras/models/mixins.py:113
 msgid "Extension to append to the rendered filename"
 msgid "Extension to append to the rendered filename"
 msgstr ""
 msgstr ""
 
 
-#: netbox/extras/models/mixins.py:115
+#: netbox/extras/models/mixins.py:116
 msgid "as attachment"
 msgid "as attachment"
 msgstr ""
 msgstr ""
 
 
-#: netbox/extras/models/mixins.py:117
+#: netbox/extras/models/mixins.py:118
 msgid "Download file as attachment"
 msgid "Download file as attachment"
 msgstr ""
 msgstr ""
 
 
-#: netbox/extras/models/mixins.py:124
+#: netbox/extras/models/mixins.py:125
 #, python-brace-format
 #, python-brace-format
 msgid "{class_name} must implement a get_context() method."
 msgid "{class_name} must implement a get_context() method."
 msgstr ""
 msgstr ""
@@ -9855,7 +9885,7 @@ msgstr ""
 #: netbox/ipam/filtersets.py:466 netbox/ipam/filtersets.py:470
 #: netbox/ipam/filtersets.py:466 netbox/ipam/filtersets.py:470
 #: netbox/ipam/filtersets.py:562 netbox/ipam/forms/model_forms.py:506
 #: netbox/ipam/filtersets.py:562 netbox/ipam/forms/model_forms.py:506
 #: netbox/templates/tenancy/contact.html:63
 #: netbox/templates/tenancy/contact.html:63
-#: netbox/tenancy/forms/bulk_edit.py:120
+#: netbox/tenancy/forms/bulk_edit.py:125
 msgid "Address"
 msgid "Address"
 msgstr ""
 msgstr ""
 
 
@@ -12311,7 +12341,7 @@ msgstr ""
 
 
 #: netbox/templates/account/profile.html:27
 #: netbox/templates/account/profile.html:27
 #: netbox/templates/tenancy/contact.html:53 netbox/templates/users/user.html:23
 #: netbox/templates/tenancy/contact.html:53 netbox/templates/users/user.html:23
-#: netbox/tenancy/forms/bulk_edit.py:116
+#: netbox/tenancy/forms/bulk_edit.py:121
 msgid "Email"
 msgid "Email"
 msgstr ""
 msgstr ""
 
 
@@ -14826,7 +14856,7 @@ msgid ""
 msgstr ""
 msgstr ""
 
 
 #: netbox/templates/tenancy/contact.html:18 netbox/tenancy/filtersets.py:152
 #: netbox/templates/tenancy/contact.html:18 netbox/tenancy/filtersets.py:152
-#: netbox/tenancy/forms/bulk_edit.py:149 netbox/tenancy/forms/filtersets.py:102
+#: netbox/tenancy/forms/bulk_edit.py:154 netbox/tenancy/forms/filtersets.py:102
 #: netbox/tenancy/forms/forms.py:57 netbox/tenancy/forms/model_forms.py:108
 #: netbox/tenancy/forms/forms.py:57 netbox/tenancy/forms/model_forms.py:108
 #: netbox/tenancy/forms/model_forms.py:132
 #: netbox/tenancy/forms/model_forms.py:132
 #: netbox/tenancy/tables/contacts.py:106
 #: netbox/tenancy/tables/contacts.py:106
@@ -14834,12 +14864,12 @@ msgid "Contact"
 msgstr ""
 msgstr ""
 
 
 #: netbox/templates/tenancy/contact.html:39
 #: netbox/templates/tenancy/contact.html:39
-#: netbox/tenancy/forms/bulk_edit.py:106
+#: netbox/tenancy/forms/bulk_edit.py:111
 msgid "Title"
 msgid "Title"
 msgstr ""
 msgstr ""
 
 
 #: netbox/templates/tenancy/contact.html:43
 #: netbox/templates/tenancy/contact.html:43
-#: netbox/tenancy/forms/bulk_edit.py:111 netbox/tenancy/tables/contacts.py:72
+#: netbox/tenancy/forms/bulk_edit.py:116 netbox/tenancy/tables/contacts.py:72
 msgid "Phone"
 msgid "Phone"
 msgstr ""
 msgstr ""
 
 
@@ -15208,15 +15238,15 @@ msgstr ""
 msgid "Tenant Group (slug)"
 msgid "Tenant Group (slug)"
 msgstr ""
 msgstr ""
 
 
-#: netbox/tenancy/forms/bulk_edit.py:67
+#: netbox/tenancy/forms/bulk_edit.py:72
 msgid "Desciption"
 msgid "Desciption"
 msgstr ""
 msgstr ""
 
 
-#: netbox/tenancy/forms/bulk_edit.py:96
+#: netbox/tenancy/forms/bulk_edit.py:101
 msgid "Add groups"
 msgid "Add groups"
 msgstr ""
 msgstr ""
 
 
-#: netbox/tenancy/forms/bulk_edit.py:101
+#: netbox/tenancy/forms/bulk_edit.py:106
 msgid "Remove groups"
 msgid "Remove groups"
 msgstr ""
 msgstr ""
 
 
@@ -16673,10 +16703,6 @@ msgstr ""
 msgid "Bridged VLAN"
 msgid "Bridged VLAN"
 msgstr ""
 msgstr ""
 
 
-#: netbox/wireless/forms/bulk_import.py:94
-msgid "Site of parent device A (if any)"
-msgstr ""
-
 #: netbox/wireless/forms/bulk_import.py:100
 #: netbox/wireless/forms/bulk_import.py:100
 msgid "Parent device of assigned interface A"
 msgid "Parent device of assigned interface A"
 msgstr ""
 msgstr ""
@@ -16690,10 +16716,6 @@ msgstr ""
 msgid "Assigned interface A"
 msgid "Assigned interface A"
 msgstr ""
 msgstr ""
 
 
-#: netbox/wireless/forms/bulk_import.py:115
-msgid "Site of parent device B (if any)"
-msgstr ""
-
 #: netbox/wireless/forms/bulk_import.py:121
 #: netbox/wireless/forms/bulk_import.py:121
 msgid "Parent device of assigned interface B"
 msgid "Parent device of assigned interface B"
 msgstr ""
 msgstr ""

BIN
netbox/translations/es/LC_MESSAGES/django.mo


File diff suppressed because it is too large
+ 208 - 170
netbox/translations/es/LC_MESSAGES/django.po


BIN
netbox/translations/fr/LC_MESSAGES/django.mo


File diff suppressed because it is too large
+ 208 - 170
netbox/translations/fr/LC_MESSAGES/django.po


BIN
netbox/translations/it/LC_MESSAGES/django.mo


File diff suppressed because it is too large
+ 208 - 170
netbox/translations/it/LC_MESSAGES/django.po


BIN
netbox/translations/ja/LC_MESSAGES/django.mo


File diff suppressed because it is too large
+ 204 - 170
netbox/translations/ja/LC_MESSAGES/django.po


BIN
netbox/translations/nl/LC_MESSAGES/django.mo


File diff suppressed because it is too large
+ 208 - 170
netbox/translations/nl/LC_MESSAGES/django.po


BIN
netbox/translations/pl/LC_MESSAGES/django.mo


File diff suppressed because it is too large
+ 208 - 172
netbox/translations/pl/LC_MESSAGES/django.po


BIN
netbox/translations/pt/LC_MESSAGES/django.mo


File diff suppressed because it is too large
+ 208 - 170
netbox/translations/pt/LC_MESSAGES/django.po


BIN
netbox/translations/ru/LC_MESSAGES/django.mo


File diff suppressed because it is too large
+ 208 - 170
netbox/translations/ru/LC_MESSAGES/django.po


BIN
netbox/translations/tr/LC_MESSAGES/django.mo


File diff suppressed because it is too large
+ 210 - 174
netbox/translations/tr/LC_MESSAGES/django.po


BIN
netbox/translations/uk/LC_MESSAGES/django.mo


File diff suppressed because it is too large
+ 208 - 170
netbox/translations/uk/LC_MESSAGES/django.po


BIN
netbox/translations/zh/LC_MESSAGES/django.mo


File diff suppressed because it is too large
+ 205 - 170
netbox/translations/zh/LC_MESSAGES/django.po


+ 17 - 0
netbox/users/migrations/0010_add_token_meta_ordering.py

@@ -0,0 +1,17 @@
+# Generated by Django 5.2.4 on 2025-07-23 17:28
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('users', '0009_update_group_perms'),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name='token',
+            options={'ordering': ('-created',)},
+        ),
+    ]

+ 1 - 1
netbox/users/migrations/0010_concrete_objecttype.py → netbox/users/migrations/0011_concrete_objecttype.py

@@ -5,7 +5,7 @@ class Migration(migrations.Migration):
 
 
     dependencies = [
     dependencies = [
         ('contenttypes', '0002_remove_content_type_name'),
         ('contenttypes', '0002_remove_content_type_name'),
-        ('users', '0009_update_group_perms'),
+        ('users', '0010_add_token_meta_ordering'),
     ]
     ]
 
 
     operations = [
     operations = [

+ 1 - 0
netbox/users/models/tokens.py

@@ -74,6 +74,7 @@ class Token(models.Model):
     class Meta:
     class Meta:
         verbose_name = _('token')
         verbose_name = _('token')
         verbose_name_plural = _('tokens')
         verbose_name_plural = _('tokens')
+        ordering = ('-created',)
 
 
     def __str__(self):
     def __str__(self):
         return self.key if settings.ALLOW_TOKEN_RETRIEVAL else self.partial
         return self.key if settings.ALLOW_TOKEN_RETRIEVAL else self.partial

+ 2 - 0
netbox/utilities/constants.py

@@ -13,6 +13,8 @@ FILTER_CHAR_BASED_LOOKUP_MAP = dict(
     ie='iexact',
     ie='iexact',
     nie='iexact',
     nie='iexact',
     empty='empty',
     empty='empty',
+    regex='regex',
+    iregex='iregex',
 )
 )
 
 
 FILTER_NUMERIC_BASED_LOOKUP_MAP = dict(
 FILTER_NUMERIC_BASED_LOOKUP_MAP = dict(

+ 3 - 0
netbox/utilities/query.py

@@ -67,5 +67,8 @@ def reapply_model_ordering(queryset: QuerySet) -> QuerySet:
     # MPTT-based models are exempt from this; use caution when annotating querysets of these models
     # MPTT-based models are exempt from this; use caution when annotating querysets of these models
     if any(isinstance(manager, TreeManager) for manager in queryset.model._meta.local_managers):
     if any(isinstance(manager, TreeManager) for manager in queryset.model._meta.local_managers):
         return queryset
         return queryset
+    elif queryset.ordered:
+        return queryset
+
     ordering = queryset.model._meta.ordering
     ordering = queryset.model._meta.ordering
     return queryset.order_by(*ordering)
     return queryset.order_by(*ordering)

+ 32 - 0
netbox/utilities/tests/test_filters.py

@@ -180,6 +180,10 @@ class BaseFilterSetTest(TestCase):
         self.assertEqual(self.filters['charfield__niew'].exclude, True)
         self.assertEqual(self.filters['charfield__niew'].exclude, True)
         self.assertEqual(self.filters['charfield__empty'].lookup_expr, 'empty')
         self.assertEqual(self.filters['charfield__empty'].lookup_expr, 'empty')
         self.assertEqual(self.filters['charfield__empty'].exclude, False)
         self.assertEqual(self.filters['charfield__empty'].exclude, False)
+        self.assertEqual(self.filters['charfield__regex'].lookup_expr, 'regex')
+        self.assertEqual(self.filters['charfield__regex'].exclude, False)
+        self.assertEqual(self.filters['charfield__iregex'].lookup_expr, 'iregex')
+        self.assertEqual(self.filters['charfield__iregex'].exclude, False)
 
 
     def test_number_filter(self):
     def test_number_filter(self):
         self.assertIsInstance(self.filters['numberfield'], django_filters.NumberFilter)
         self.assertIsInstance(self.filters['numberfield'], django_filters.NumberFilter)
@@ -220,6 +224,10 @@ class BaseFilterSetTest(TestCase):
         self.assertEqual(self.filters['macaddressfield__iew'].exclude, False)
         self.assertEqual(self.filters['macaddressfield__iew'].exclude, False)
         self.assertEqual(self.filters['macaddressfield__niew'].lookup_expr, 'iendswith')
         self.assertEqual(self.filters['macaddressfield__niew'].lookup_expr, 'iendswith')
         self.assertEqual(self.filters['macaddressfield__niew'].exclude, True)
         self.assertEqual(self.filters['macaddressfield__niew'].exclude, True)
+        self.assertEqual(self.filters['macaddressfield__regex'].lookup_expr, 'regex')
+        self.assertEqual(self.filters['macaddressfield__regex'].exclude, False)
+        self.assertEqual(self.filters['macaddressfield__iregex'].lookup_expr, 'iregex')
+        self.assertEqual(self.filters['macaddressfield__iregex'].exclude, False)
 
 
     def test_model_choice_filter(self):
     def test_model_choice_filter(self):
         self.assertIsInstance(self.filters['modelchoicefield'], django_filters.ModelChoiceFilter)
         self.assertIsInstance(self.filters['modelchoicefield'], django_filters.ModelChoiceFilter)
@@ -257,6 +265,10 @@ class BaseFilterSetTest(TestCase):
         self.assertEqual(self.filters['multivaluecharfield__iew'].exclude, False)
         self.assertEqual(self.filters['multivaluecharfield__iew'].exclude, False)
         self.assertEqual(self.filters['multivaluecharfield__niew'].lookup_expr, 'iendswith')
         self.assertEqual(self.filters['multivaluecharfield__niew'].lookup_expr, 'iendswith')
         self.assertEqual(self.filters['multivaluecharfield__niew'].exclude, True)
         self.assertEqual(self.filters['multivaluecharfield__niew'].exclude, True)
+        self.assertEqual(self.filters['multivaluecharfield__regex'].lookup_expr, 'regex')
+        self.assertEqual(self.filters['multivaluecharfield__regex'].exclude, False)
+        self.assertEqual(self.filters['multivaluecharfield__iregex'].lookup_expr, 'iregex')
+        self.assertEqual(self.filters['multivaluecharfield__iregex'].exclude, False)
 
 
     def test_multi_value_date_filter(self):
     def test_multi_value_date_filter(self):
         self.assertIsInstance(self.filters['datefield'], MultiValueDateFilter)
         self.assertIsInstance(self.filters['datefield'], MultiValueDateFilter)
@@ -340,6 +352,10 @@ class BaseFilterSetTest(TestCase):
         self.assertEqual(self.filters['multiplechoicefield__iew'].exclude, False)
         self.assertEqual(self.filters['multiplechoicefield__iew'].exclude, False)
         self.assertEqual(self.filters['multiplechoicefield__niew'].lookup_expr, 'iendswith')
         self.assertEqual(self.filters['multiplechoicefield__niew'].lookup_expr, 'iendswith')
         self.assertEqual(self.filters['multiplechoicefield__niew'].exclude, True)
         self.assertEqual(self.filters['multiplechoicefield__niew'].exclude, True)
+        self.assertEqual(self.filters['multiplechoicefield__regex'].lookup_expr, 'regex')
+        self.assertEqual(self.filters['multiplechoicefield__regex'].exclude, False)
+        self.assertEqual(self.filters['multiplechoicefield__iregex'].lookup_expr, 'iregex')
+        self.assertEqual(self.filters['multiplechoicefield__iregex'].exclude, False)
 
 
     def test_tag_filter(self):
     def test_tag_filter(self):
         self.assertIsInstance(self.filters['tagfield'], TagFilter)
         self.assertIsInstance(self.filters['tagfield'], TagFilter)
@@ -534,6 +550,14 @@ class DynamicFilterLookupExpressionTest(TestCase):
         params = {'slug__niew': ['-1']}
         params = {'slug__niew': ['-1']}
         self.assertEqual(SiteFilterSet(params, Site.objects.all()).qs.count(), 2)
         self.assertEqual(SiteFilterSet(params, Site.objects.all()).qs.count(), 2)
 
 
+    def test_site_slug_regex(self):
+        params = {'slug__regex': ['^def-[a-z]*-2$']}
+        self.assertEqual(SiteFilterSet(params, Site.objects.all()).qs.count(), 1)
+
+    def test_site_slug_iregex(self):
+        params = {'slug__iregex': ['^DEF-[a-z]*-2$']}
+        self.assertEqual(SiteFilterSet(params, Site.objects.all()).qs.count(), 1)
+
     def test_provider_asn_lt(self):
     def test_provider_asn_lt(self):
         params = {'asn__lt': [65101]}
         params = {'asn__lt': [65101]}
         self.assertEqual(ASNFilterSet(params, ASN.objects.all()).qs.count(), 1)
         self.assertEqual(ASNFilterSet(params, ASN.objects.all()).qs.count(), 1)
@@ -618,6 +642,14 @@ class DynamicFilterLookupExpressionTest(TestCase):
         params = {'mac_address__nic': ['aa:', 'bb']}
         params = {'mac_address__nic': ['aa:', 'bb']}
         self.assertEqual(DeviceFilterSet(params, Device.objects.all()).qs.count(), 1)
         self.assertEqual(DeviceFilterSet(params, Device.objects.all()).qs.count(), 1)
 
 
+    def test_device_mac_address_regex(self):
+        params = {'mac_address__regex': ['^cc.*:03$']}
+        self.assertEqual(DeviceFilterSet(params, Device.objects.all()).qs.count(), 1)
+
+    def test_device_mac_address_iregex(self):
+        params = {'mac_address__iregex': ['^CC.*:03$']}
+        self.assertEqual(DeviceFilterSet(params, Device.objects.all()).qs.count(), 1)
+
     def test_interface_rf_role_empty(self):
     def test_interface_rf_role_empty(self):
         params = {'rf_role__empty': 'true'}
         params = {'rf_role__empty': 'true'}
         self.assertEqual(InterfaceFilterSet(params, Interface.objects.all()).qs.count(), 5)
         self.assertEqual(InterfaceFilterSet(params, Interface.objects.all()).qs.count(), 5)

+ 1 - 1
pyproject.toml

@@ -3,7 +3,7 @@
 
 
 [project]
 [project]
 name = "netbox"
 name = "netbox"
-version = "4.3.4"
+version = "4.3.5"
 requires-python = ">=3.10"
 requires-python = ">=3.10"
 authors = [
 authors = [
     { name = "NetBox Community" }
     { name = "NetBox Community" }

+ 7 - 7
requirements.txt

@@ -20,21 +20,21 @@ drf-spectacular-sidecar==2025.7.1
 feedparser==6.0.11
 feedparser==6.0.11
 gunicorn==23.0.0
 gunicorn==23.0.0
 Jinja2==3.1.6
 Jinja2==3.1.6
-jsonschema==4.24.0
+jsonschema==4.25.0
 Markdown==3.8.2
 Markdown==3.8.2
-mkdocs-material==9.6.15
-mkdocstrings[python]==0.29.1
+mkdocs-material==9.6.16
+mkdocstrings[python]==0.30.0
 netaddr==1.3.0
 netaddr==1.3.0
-nh3==0.2.22
+nh3==0.3.0
 Pillow==11.3.0
 Pillow==11.3.0
 psycopg[c,pool]==3.2.9
 psycopg[c,pool]==3.2.9
 PyYAML==6.0.2
 PyYAML==6.0.2
 requests==2.32.4
 requests==2.32.4
-rq==2.4.0
+rq==2.4.1
 social-auth-app-django==5.5.1
 social-auth-app-django==5.5.1
 social-auth-core==4.7.0
 social-auth-core==4.7.0
-strawberry-graphql==0.276.0
-strawberry-graphql-django==0.60.0
+strawberry-graphql==0.278.0
+strawberry-graphql-django==0.65.1
 svgwrite==1.4.3
 svgwrite==1.4.3
 tablib==3.8.0
 tablib==3.8.0
 tzdata==2025.2
 tzdata==2025.2

Some files were not shown because too many files changed in this diff