Просмотр исходного кода

Merge branch 'develop' into feature

Jeremy Stretch 1 год назад
Родитель
Сommit
f08e36e538
68 измененных файлов с 3625 добавлено и 3072 удалено
  1. 1 1
      .github/ISSUE_TEMPLATE/01-feature_request.yaml
  2. 1 1
      .github/ISSUE_TEMPLATE/02-bug_report.yaml
  3. 3 0
      .github/ISSUE_TEMPLATE/config.yml
  4. 5 0
      .github/workflows/ci.yml
  5. 12 0
      .tx/config
  6. 2 2
      base_requirements.txt
  7. 1 0
      contrib/generated_schema.json
  8. 14 1
      docs/development/release-checklist.md
  9. 13 8
      docs/development/translations.md
  10. BIN
      docs/media/development/transifex_pull_request.png
  11. BIN
      docs/media/development/transifex_sync.png
  12. 1 1
      docs/models/vpn/ikepolicy.md
  13. 32 0
      docs/release-notes/version-4.1.md
  14. 6 1
      netbox/core/models/jobs.py
  15. 30 0
      netbox/core/tests/test_views.py
  16. 1 5
      netbox/core/views.py
  17. 2 0
      netbox/dcim/choices.py
  18. 57 19
      netbox/dcim/forms/bulk_edit.py
  19. 7 0
      netbox/dcim/forms/model_forms.py
  20. 0 8
      netbox/dcim/forms/object_create.py
  21. 11 1
      netbox/dcim/views.py
  22. 1 3
      netbox/extras/jobs.py
  23. 0 1
      netbox/extras/views.py
  24. 21 5
      netbox/ipam/models/vlans.py
  25. 0 1
      netbox/netbox/graphql/views.py
  26. 1 0
      netbox/netbox/jobs.py
  27. 19 1
      netbox/netbox/tests/test_views.py
  28. 2 3
      netbox/netbox/urls.py
  29. 13 6
      netbox/netbox/views/generic/bulk_views.py
  30. 10 0
      netbox/netbox/views/misc.py
  31. 1 1
      netbox/project-static/package.json
  32. 4 4
      netbox/project-static/yarn.lock
  33. 2 2
      netbox/release.yaml
  34. 1 1
      netbox/templates/core/datasource.html
  35. 2 12
      netbox/templates/graphql/graphiql.html
  36. BIN
      netbox/translations/cs/LC_MESSAGES/django.mo
  37. 224 198
      netbox/translations/cs/LC_MESSAGES/django.po
  38. BIN
      netbox/translations/da/LC_MESSAGES/django.mo
  39. 224 198
      netbox/translations/da/LC_MESSAGES/django.po
  40. BIN
      netbox/translations/de/LC_MESSAGES/django.mo
  41. 224 198
      netbox/translations/de/LC_MESSAGES/django.po
  42. 225 201
      netbox/translations/en/LC_MESSAGES/django.po
  43. BIN
      netbox/translations/es/LC_MESSAGES/django.mo
  44. 224 198
      netbox/translations/es/LC_MESSAGES/django.po
  45. BIN
      netbox/translations/fr/LC_MESSAGES/django.mo
  46. 224 198
      netbox/translations/fr/LC_MESSAGES/django.po
  47. BIN
      netbox/translations/it/LC_MESSAGES/django.mo
  48. 224 198
      netbox/translations/it/LC_MESSAGES/django.po
  49. BIN
      netbox/translations/ja/LC_MESSAGES/django.mo
  50. 226 200
      netbox/translations/ja/LC_MESSAGES/django.po
  51. BIN
      netbox/translations/nl/LC_MESSAGES/django.mo
  52. 225 198
      netbox/translations/nl/LC_MESSAGES/django.po
  53. BIN
      netbox/translations/pl/LC_MESSAGES/django.mo
  54. 224 198
      netbox/translations/pl/LC_MESSAGES/django.po
  55. BIN
      netbox/translations/pt/LC_MESSAGES/django.mo
  56. 226 200
      netbox/translations/pt/LC_MESSAGES/django.po
  57. BIN
      netbox/translations/ru/LC_MESSAGES/django.mo
  58. 224 198
      netbox/translations/ru/LC_MESSAGES/django.po
  59. BIN
      netbox/translations/tr/LC_MESSAGES/django.mo
  60. 224 198
      netbox/translations/tr/LC_MESSAGES/django.po
  61. BIN
      netbox/translations/uk/LC_MESSAGES/django.mo
  62. 189 187
      netbox/translations/uk/LC_MESSAGES/django.po
  63. BIN
      netbox/translations/zh/LC_MESSAGES/django.mo
  64. 224 198
      netbox/translations/zh/LC_MESSAGES/django.po
  65. 6 6
      netbox/utilities/conversion.py
  66. 2 2
      netbox/virtualization/forms/bulk_edit.py
  67. 1 1
      netbox/virtualization/forms/filtersets.py
  68. 9 9
      requirements.txt

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

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

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

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

+ 3 - 0
.github/ISSUE_TEMPLATE/config.yml

@@ -7,6 +7,9 @@ contact_links:
   - name: ❓ Discussion
     url: https://github.com/netbox-community/netbox/discussions
     about: "If you're just looking for help, try starting a discussion instead."
+  - name: 👔 Professional Support
+    url: https://netboxlabs.com/netbox-enterprise/
+    about: "Professional support is available for NetBox Enterprise or Cloud."
   - name: 🌎 Correct a Translation
     url: https://explore.transifex.com/netbox-community/netbox/
     about: "Spot an incorrect translation? You can propose a fix on Transifex."

+ 5 - 0
.github/workflows/ci.yml

@@ -15,6 +15,11 @@ on:
 permissions:
   contents: read
 
+# Add concurrency group to control job running
+concurrency:
+  group: ${{ github.event_name }}-${{ github.ref }}-${{ github.actor }}
+  cancel-in-progress: true
+
 jobs:
   build:
     runs-on: ubuntu-latest

+ 12 - 0
.tx/config

@@ -0,0 +1,12 @@
+[main]
+host = https://app.transifex.com
+
+[o:netbox-community:p:netbox:r:9cbf4fcf95b3d92e4ebbf1a5e5d1caee]
+file_filter            = netbox/translations/<lang>/LC_MESSAGES/django.po
+source_file            = netbox/translations/en/LC_MESSAGES/django.po
+type                   = PO
+minimum_perc           = 0
+resource_name          = django.po
+replace_edited_strings = false
+keep_translations      = false
+

+ 2 - 2
base_requirements.txt

@@ -42,7 +42,7 @@ django-rich
 
 # Django integration for RQ (Reqis queuing)
 # https://github.com/rq/django-rq/blob/master/CHANGELOG.md
-django-rq<3.0
+django-rq
 
 # Abstraction models for rendering and paginating HTML tables
 # https://github.com/jieter/django-tables2/blob/master/CHANGELOG.md
@@ -118,7 +118,7 @@ requests
 
 # rq
 # https://github.com/rq/rq/blob/master/CHANGES.md
-rq<2.0
+rq
 
 # Social authentication framework
 # https://github.com/python-social-auth/social-core/blob/master/CHANGELOG.md

+ 1 - 0
contrib/generated_schema.json

@@ -329,6 +329,7 @@
                         "100base-tx",
                         "100base-t1",
                         "1000base-t",
+                        "1000base-lx",
                         "1000base-tx",
                         "2.5gbase-t",
                         "5gbase-t",

+ 14 - 1
docs/development/release-checklist.md

@@ -90,7 +90,20 @@ This will automatically update the schema file at `contrib/generated_schema.json
 
 ### Update & Compile Translations
 
-Updated language translations should be pulled from [Transifex](https://app.transifex.com/netbox-community/netbox/dashboard/) and re-compiled for each new release. Follow the documented process for [updating translated strings](./translations.md#updating-translated-strings) to do this.
+Updated language translations should be pulled from [Transifex](https://app.transifex.com/netbox-community/netbox/dashboard/) and re-compiled for each new release. First, retrieve any updated translation files using the Transifex CLI client:
+
+```no-highlight
+tx pull
+```
+
+Then, compile these portable (`.po`) files for use in the application:
+
+```no-highlight
+./manage.py compilemessages
+```
+
+!!! tip
+    Consult the translation documentation for more detail on [updating translated strings](./translations.md#updating-translated-strings) if you've not set up the Transifex client already.
 
 ### Update Version and Changelog
 

+ 13 - 8
docs/development/translations.md

@@ -16,26 +16,31 @@ To update the English `.po` file from which all translations are derived, use th
 
 Then, commit the change and push to the `develop` branch on GitHub. Any new strings will appear for translation on Transifex automatically.
 
+!!! note
+    It is typically not necessary to update source strings manually, as this is done nightly by a [GitHub action](https://github.com/netbox-community/netbox/blob/develop/.github/workflows/update-translation-strings.yml).
+
 ## Updating Translated Strings
 
 Typically, translated strings need to be updated only as part of the NetBox [release process](./release-checklist.md).
 
 Check the Transifex dashboard for languages that are not marked _ready for use_, being sure to click _Show all languages_ if it appears at the bottom of the list. Use machine translation to round out any not-ready languages. It's not necessary to review the machine translation immediately as the translation teams will handle that aspect; the goal at this stage is to get translations included in the Transifex pull request.
 
-To update translated strings, start by initiating a sync from Transifex. From the Transifex dashboard, navigate to Settings > Integrations > GitHub > Manage, and click the **Manual Sync** button at top right.
+To download translated strings automatically, you'll need to:
 
-![Transifex manual sync](../media/development/transifex_sync.png)
+1. Install the [Transifex CLI client](https://github.com/transifex/cli)
+2. Generate a [Transifex API token](https://app.transifex.com/user/settings/api/)
 
-Enter a threshold percentage of 1 (to ensure all translations are captured) and select the `develop` branch, then click **Sync**. This will initiate a pull request to GitHub to update any newly modified translation (`.po`) files.
+Once you have the client set up, run the following command:
 
-!!! tip
-    The new PR should appear within a few minutes. If it does not, check that there are in fact new translations to be added.
+```no-highlight
+TX_TOKEN=$TOKEN tx pull
+```
 
-![Transifex pull request](../media/development/transifex_pull_request.png)
+This will download all portable (`.po`) translation files from Transifex, updating them locally as needed.
 
-Once the PR has been merged, the updated strings need to be compiled into new `.mo` files so they can be used by the application. Update the `develop` branch locally to pull in the changes from the Transifex PR, then run Django's [`compilemessages`](https://docs.djangoproject.com/en/stable/ref/django-admin/#django-admin-compilemessages) management command:
+Once retrieved, the updated strings need to be compiled into new `.mo` files so they can be used by the application. Run Django's [`compilemessages`](https://docs.djangoproject.com/en/stable/ref/django-admin/#django-admin-compilemessages) management command to compile them:
 
-```nohighlight
+```no-highlight
 ./manage.py compilemessages
 ```
 

BIN
docs/media/development/transifex_pull_request.png


BIN
docs/media/development/transifex_sync.png


+ 1 - 1
docs/models/vpn/ikepolicy.md

@@ -1,6 +1,6 @@
 # IKE Policies
 
-An [Internet Key Exhcnage (IKE)](https://en.wikipedia.org/wiki/Internet_Key_Exchange) policy defines an IKE version, mode, and set of [proposals](./ikeproposal.md) to be used in IKE negotiation. These policies are referenced by [IPSec profiles](./ipsecprofile.md).
+An [Internet Key Exchange (IKE)](https://en.wikipedia.org/wiki/Internet_Key_Exchange) policy defines an IKE version, mode, and set of [proposals](./ikeproposal.md) to be used in IKE negotiation. These policies are referenced by [IPSec profiles](./ipsecprofile.md).
 
 ## Fields
 

+ 32 - 0
docs/release-notes/version-4.1.md

@@ -1,5 +1,37 @@
 # NetBox v4.1
 
+## v4.1.7 (FUTURE)
+
+### Enhancements
+
+* [#15239](https://github.com/netbox-community/netbox/issues/15239) - Enable adding/removing individual VLANs while bulk editing device interfaces
+* [#17871](https://github.com/netbox-community/netbox/issues/17871) - Enable the assignment/removal of virtualization cluster via device bulk edit
+* [#17934](https://github.com/netbox-community/netbox/issues/17934) - Add 1000Base-LX interface type
+* [#18007](https://github.com/netbox-community/netbox/issues/18007) - Hide sensitive parameters under data source view (even for privileged users)
+
+### Bug Fixes
+
+* [#17459](https://github.com/netbox-community/netbox/issues/17459) - Correct help text on `name` field of module type component templates
+* [#17901](https://github.com/netbox-community/netbox/issues/17901) - Ensure GraphiQL UI resources are served locally
+* [#17921](https://github.com/netbox-community/netbox/issues/17921) - Fix scheduling of recurring custom scripts
+* [#17923](https://github.com/netbox-community/netbox/issues/17923) - Fix the execution of custom scripts via REST API & management command
+* [#17963](https://github.com/netbox-community/netbox/issues/17963) - Fix selection of all listed objects during bulk edit
+* [#17969](https://github.com/netbox-community/netbox/issues/17969) - Fix system info export when a config revision exists
+* [#17972](https://github.com/netbox-community/netbox/issues/17972) - Force evaluation of `LOGIN_REQUIRED` when requesting static media
+* [#17986](https://github.com/netbox-community/netbox/issues/17986) - Correct labels for virtual machine & virtual disk size properties
+* [#18037](https://github.com/netbox-community/netbox/issues/18037) - Fix validation of maximum VLAN ID value when defining VLAN groups
+* [#18038](https://github.com/netbox-community/netbox/issues/18038) - The `to_grams()` utility function should always return an integer value
+
+---
+
+## v4.1.6 (2024-10-31)
+
+### Bug Fixes
+
+* [#17700](https://github.com/netbox-community/netbox/issues/17700) - Fix warning when no scripts are found within a script module
+* [#17884](https://github.com/netbox-community/netbox/issues/17884) - Fix translation support for certain tab headings
+* [#17885](https://github.com/netbox-community/netbox/issues/17885) - Fix regression preventing custom scripts from executing
+
 ## v4.1.5 (2024-10-28)
 
 ### Enhancements

+ 6 - 1
netbox/core/models/jobs.py

@@ -9,6 +9,7 @@ from django.db import models
 from django.urls import reverse
 from django.utils import timezone
 from django.utils.translation import gettext as _
+from rq.exceptions import InvalidJobOperation
 
 from core.choices import JobStatusChoices
 from core.models import ObjectType
@@ -158,7 +159,11 @@ class Job(models.Model):
         job = queue.fetch_job(str(self.job_id))
 
         if job:
-            job.cancel()
+            try:
+                job.cancel()
+            except InvalidJobOperation:
+                # Job may raise this exception from get_status() if missing from Redis
+                pass
 
     def start(self):
         """

+ 30 - 0
netbox/core/tests/test_views.py

@@ -308,6 +308,7 @@ class BackgroundTaskTestCase(TestCase):
         worker = get_worker('default')
         job = queue.enqueue(self.dummy_job_default)
         worker.prepare_job_execution(job)
+        worker.prepare_execution(job)
 
         self.assertEqual(job.get_status(), JobStatus.STARTED)
 
@@ -345,3 +346,32 @@ class BackgroundTaskTestCase(TestCase):
         self.assertIn(str(worker1.name), str(response.content))
         self.assertIn('Birth', str(response.content))
         self.assertIn('Total working time', str(response.content))
+
+
+class SystemTestCase(TestCase):
+
+    def setUp(self):
+        super().setUp()
+
+        self.user.is_staff = True
+        self.user.save()
+
+    def test_system_view_default(self):
+        # Test UI render
+        response = self.client.get(reverse('core:system'))
+        self.assertEqual(response.status_code, 200)
+
+        # Test export
+        response = self.client.get(f"{reverse('core:system')}?export=true")
+        self.assertEqual(response.status_code, 200)
+
+    def test_system_view_with_config_revision(self):
+        ConfigRevision.objects.create()
+
+        # Test UI render
+        response = self.client.get(reverse('core:system'))
+        self.assertEqual(response.status_code, 200)
+
+        # Test export
+        response = self.client.get(f"{reverse('core:system')}?export=true")
+        self.assertEqual(response.status_code, 200)

+ 1 - 5
netbox/core/views.py

@@ -642,11 +642,7 @@ class SystemView(UserPassesTestMixin, View):
         }
 
         # Configuration
-        try:
-            config = ConfigRevision.objects.get(pk=cache.get('config_version'))
-        except ConfigRevision.DoesNotExist:
-            # Fall back to using the active config data if no record is found
-            config = get_config()
+        config = get_config()
 
         # Raw data export
         if 'export' in request.GET:

+ 2 - 0
netbox/dcim/choices.py

@@ -871,6 +871,7 @@ class InterfaceTypeChoices(ChoiceSet):
     TYPE_100ME_T1 = '100base-t1'
     TYPE_100ME_SFP = '100base-x-sfp'
     TYPE_1GE_FIXED = '1000base-t'
+    TYPE_1GE_LX_FIXED = '1000base-lx'
     TYPE_1GE_TX_FIXED = '1000base-tx'
     TYPE_1GE_GBIC = '1000base-x-gbic'
     TYPE_1GE_SFP = '1000base-x-sfp'
@@ -1033,6 +1034,7 @@ class InterfaceTypeChoices(ChoiceSet):
                 (TYPE_100ME_FIXED, '100BASE-TX (10/100ME)'),
                 (TYPE_100ME_T1, '100BASE-T1 (10/100ME Single Pair)'),
                 (TYPE_1GE_FIXED, '1000BASE-T (1GE)'),
+                (TYPE_1GE_LX_FIXED, '1000BASE-LX (1GE)'),
                 (TYPE_1GE_TX_FIXED, '1000BASE-TX (1GE)'),
                 (TYPE_2GE_FIXED, '2.5GBASE-T (2.5GE)'),
                 (TYPE_5GE_FIXED, '5GBASE-T (5GE)'),

+ 57 - 19
netbox/dcim/forms/bulk_edit.py

@@ -14,10 +14,11 @@ from tenancy.models import Tenant
 from users.models import User
 from utilities.forms import BulkEditForm, add_blank_choice, form_from_model
 from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
-from utilities.forms.rendering import FieldSet, InlineFields
+from utilities.forms.rendering import FieldSet, InlineFields, TabbedGroups
 from utilities.forms.widgets import BulkEditNullBooleanSelect, NumberWithOptions
-from wireless.models import WirelessLAN, WirelessLANGroup
+from virtualization.models import Cluster
 from wireless.choices import WirelessRoleChoices
+from wireless.models import WirelessLAN, WirelessLANGroup
 
 __all__ = (
     'CableBulkEditForm',
@@ -723,6 +724,14 @@ class DeviceBulkEditForm(NetBoxModelBulkEditForm):
         queryset=ConfigTemplate.objects.all(),
         required=False
     )
+    cluster = DynamicModelChoiceField(
+        label=_('Cluster'),
+        queryset=Cluster.objects.all(),
+        required=False,
+        query_params={
+            'site_id': ['$site', 'null']
+        },
+    )
     comments = CommentField()
 
     model = Device
@@ -731,9 +740,10 @@ class DeviceBulkEditForm(NetBoxModelBulkEditForm):
         FieldSet('site', 'location', name=_('Location')),
         FieldSet('manufacturer', 'device_type', 'airflow', 'serial', name=_('Hardware')),
         FieldSet('config_template', name=_('Configuration')),
+        FieldSet('cluster', name=_('Virtualization')),
     )
     nullable_fields = (
-        'location', 'tenant', 'platform', 'serial', 'airflow', 'description', 'comments',
+        'location', 'tenant', 'platform', 'serial', 'airflow', 'description', 'cluster', 'comments',
     )
 
 
@@ -1406,18 +1416,25 @@ class InterfaceBulkEditForm(
     parent = DynamicModelChoiceField(
         label=_('Parent'),
         queryset=Interface.objects.all(),
-        required=False
+        required=False,
+        query_params={
+            'virtual_chassis_member_id': '$device',
+        }
     )
     bridge = DynamicModelChoiceField(
         label=_('Bridge'),
         queryset=Interface.objects.all(),
-        required=False
+        required=False,
+        query_params={
+            'virtual_chassis_member_id': '$device',
+        }
     )
     lag = DynamicModelChoiceField(
         queryset=Interface.objects.all(),
         required=False,
         query_params={
             'type': 'lag',
+            'virtual_chassis_member_id': '$device',
         },
         label=_('LAG')
     )
@@ -1474,6 +1491,7 @@ class InterfaceBulkEditForm(
         required=False,
         query_params={
             'group_id': '$vlan_group',
+            'available_on_device': '$device',
         },
         label=_('Untagged VLAN')
     )
@@ -1482,9 +1500,28 @@ class InterfaceBulkEditForm(
         required=False,
         query_params={
             'group_id': '$vlan_group',
+            'available_on_device': '$device',
         },
         label=_('Tagged VLANs')
     )
+    add_tagged_vlans = DynamicModelMultipleChoiceField(
+        label=_('Add tagged VLANs'),
+        queryset=VLAN.objects.all(),
+        required=False,
+        query_params={
+            'group_id': '$vlan_group',
+            'available_on_device': '$device',
+        },
+    )
+    remove_tagged_vlans = DynamicModelMultipleChoiceField(
+        label=_('Remove tagged VLANs'),
+        queryset=VLAN.objects.all(),
+        required=False,
+        query_params={
+            'group_id': '$vlan_group',
+            'available_on_device': '$device',
+        }
+    )
     vrf = DynamicModelChoiceField(
         queryset=VRF.objects.all(),
         required=False,
@@ -1511,7 +1548,13 @@ class InterfaceBulkEditForm(
         FieldSet('vdcs', 'mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected', name=_('Operation')),
         FieldSet('poe_mode', 'poe_type', name=_('PoE')),
         FieldSet('parent', 'bridge', 'lag', name=_('Related Interfaces')),
-        FieldSet('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans', name=_('802.1Q Switching')),
+        FieldSet('mode', 'vlan_group', 'untagged_vlan', name=_('802.1Q Switching')),
+        FieldSet(
+            TabbedGroups(
+                FieldSet('tagged_vlans', name=_('Assignment')),
+                FieldSet('add_tagged_vlans', 'remove_tagged_vlans', name=_('Add/Remove')),
+            ),
+        ),
         FieldSet(
             'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'wireless_lan_group', 'wireless_lans',
             name=_('Wireless')
@@ -1525,19 +1568,7 @@ class InterfaceBulkEditForm(
 
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
-        if self.device_id:
-            device = Device.objects.filter(pk=self.device_id).first()
-
-            # Restrict parent/bridge/LAG interface assignment by device
-            self.fields['parent'].widget.add_query_param('virtual_chassis_member_id', device.pk)
-            self.fields['bridge'].widget.add_query_param('virtual_chassis_member_id', device.pk)
-            self.fields['lag'].widget.add_query_param('virtual_chassis_member_id', device.pk)
-
-            # Limit VLAN choices by device
-            self.fields['untagged_vlan'].widget.add_query_param('available_on_device', device.pk)
-            self.fields['tagged_vlans'].widget.add_query_param('available_on_device', device.pk)
-
-        else:
+        if not self.device_id:
             # See #4523
             if 'pk' in self.initial:
                 site = None
@@ -1561,6 +1592,13 @@ class InterfaceBulkEditForm(
                         'site_id', [site.pk, settings.FILTERS_NULL_CHOICE_VALUE]
                     )
 
+                    self.fields['add_tagged_vlans'].widget.add_query_param(
+                        'site_id', [site.pk, settings.FILTERS_NULL_CHOICE_VALUE]
+                    )
+                    self.fields['remove_tagged_vlans'].widget.add_query_param(
+                        'site_id', [site.pk, settings.FILTERS_NULL_CHOICE_VALUE]
+                    )
+
             self.fields['parent'].choices = ()
             self.fields['parent'].widget.attrs['disabled'] = True
             self.fields['bridge'].choices = ()

+ 7 - 0
netbox/dcim/forms/model_forms.py

@@ -918,6 +918,13 @@ class ModularComponentTemplateForm(ComponentTemplateForm):
         if self.instance.pk:
             self.fields['module_type'].disabled = True
 
+        # Components attached to a module need to present this standardized substitution help text.
+        self.fields['name'].help_text = _(
+            "Alphanumeric ranges are supported for bulk creation. Mixed cases and types within a single range are not "
+            "supported (example: <code>[ge,xe]-0/0/[0-9]</code>). The token <code>{module}</code>, if present, will be "
+            "automatically replaced with the position value when creating a new module."
+        )
+
 
 class ConsolePortTemplateForm(ModularComponentTemplateForm):
     fieldsets = (

+ 0 - 8
netbox/dcim/forms/object_create.py

@@ -243,14 +243,6 @@ class InterfaceCreateForm(ComponentCreateForm, model_forms.InterfaceForm):
     class Meta(model_forms.InterfaceForm.Meta):
         exclude = ('name', 'label')
 
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-
-        if 'module' in self.fields:
-            self.fields['name'].help_text += _(
-                "The string <code>{module}</code> will be replaced with the position of the assigned module, if any."
-            )
-
 
 class FrontPortCreateForm(ComponentCreateForm, model_forms.FrontPortForm):
     device = DynamicModelChoiceField(

+ 11 - 1
netbox/dcim/views.py

@@ -35,7 +35,7 @@ from virtualization.forms import VirtualMachineFilterForm
 from virtualization.models import VirtualMachine
 from virtualization.tables import VirtualMachineTable
 from . import filtersets, forms, tables
-from .choices import DeviceFaceChoices
+from .choices import DeviceFaceChoices, InterfaceModeChoices
 from .models import *
 
 CABLE_TERMINATION_TYPES = {
@@ -2792,6 +2792,16 @@ class InterfaceBulkEditView(generic.BulkEditView):
     table = tables.InterfaceTable
     form = forms.InterfaceBulkEditForm
 
+    def post_save_operations(self, form, obj):
+        super().post_save_operations(form, obj)
+
+        # Add/remove tagged VLANs
+        if obj.mode == InterfaceModeChoices.MODE_TAGGED:
+            if form.cleaned_data.get('add_tagged_vlans', None):
+                obj.tagged_vlans.add(*form.cleaned_data['add_tagged_vlans'])
+            if form.cleaned_data.get('remove_tagged_vlans', None):
+                obj.tagged_vlans.remove(*form.cleaned_data['remove_tagged_vlans'])
+
 
 @register_model_view(Interface, 'bulk_rename', path='rename', detail=False)
 class InterfaceBulkRenameView(generic.BulkRenameView):

+ 1 - 3
netbox/extras/jobs.py

@@ -22,9 +22,7 @@ class ScriptJob(JobRunner):
     """
 
     class Meta:
-        # An explicit job name is not set because it doesn't make sense in this context. Currently, there's no scenario
-        # where jobs other than this one are used. Therefore, it is hidden, resulting in a cleaner job table overview.
-        name = ''
+        name = 'Run Script'
 
     def run_script(self, script, request, data, commit):
         """

+ 0 - 1
netbox/extras/views.py

@@ -1250,7 +1250,6 @@ class ScriptView(BaseScriptView):
                 request=copy_safe_request(request),
                 job_timeout=script.python_class.job_timeout,
                 commit=form.cleaned_data.pop('_commit'),
-                name=script.name
             )
 
             return redirect('extras:script_result', job_pk=job.pk)

+ 21 - 5
netbox/ipam/models/vlans.py

@@ -96,16 +96,32 @@ class VLANGroup(OrganizationalModel):
             raise ValidationError(_("Cannot set scope_id without scope_type."))
 
         # Validate VID ranges
-        if self.vid_ranges and check_ranges_overlap(self.vid_ranges):
-            raise ValidationError({'vid_ranges': _("Ranges cannot overlap.")})
         for vid_range in self.vid_ranges:
-            if vid_range.lower > vid_range.upper:
+            lower_vid = vid_range.lower if vid_range.lower_inc else vid_range.lower + 1
+            upper_vid = vid_range.upper if vid_range.upper_inc else vid_range.upper - 1
+            if lower_vid < VLAN_VID_MIN:
+                raise ValidationError({
+                    'vid_ranges': _("Starting VLAN ID in range ({value}) cannot be less than {minimum}").format(
+                        value=lower_vid, minimum=VLAN_VID_MIN
+                    )
+                })
+            if upper_vid > VLAN_VID_MAX:
+                raise ValidationError({
+                    'vid_ranges': _("Ending VLAN ID in range ({value}) cannot exceed {maximum}").format(
+                        value=upper_vid, maximum=VLAN_VID_MAX
+                    )
+                })
+            if lower_vid > upper_vid:
                 raise ValidationError({
                     'vid_ranges': _(
-                        "Maximum child VID must be greater than or equal to minimum child VID ({value})"
-                    ).format(value=vid_range)
+                        "Ending VLAN ID in range must be greater than or equal to the starting VLAN ID ({range})"
+                    ).format(range=f'{lower_vid}-{upper_vid}')
                 })
 
+        # Check for overlapping VID ranges
+        if self.vid_ranges and check_ranges_overlap(self.vid_ranges):
+            raise ValidationError({'vid_ranges': _("Ranges cannot overlap.")})
+
     def save(self, *args, **kwargs):
         self._total_vlan_ids = 0
         for vid_range in self.vid_ranges:

+ 0 - 1
netbox/netbox/graphql/views.py

@@ -14,7 +14,6 @@ class NetBoxGraphQLView(GraphQLView):
     """
     Extends strawberry's GraphQLView to support DRF's token-based authentication.
     """
-    graphiql_template = 'graphiql.html'
 
     @csrf_exempt
     def dispatch(self, request, *args, **kwargs):

+ 1 - 0
netbox/netbox/jobs.py

@@ -91,6 +91,7 @@ class JobRunner(ABC):
                     kwargs["job_timeout"] = job.object.python_class.job_timeout
                 cls.enqueue(
                     instance=job.object,
+                    name=job.name,
                     user=job.user,
                     schedule_at=new_scheduled_time,
                     interval=job.interval,

+ 19 - 1
netbox/netbox/tests/test_views.py

@@ -1,7 +1,7 @@
 import urllib.parse
 
 from django.urls import reverse
-from django.test import override_settings
+from django.test import Client, override_settings
 
 from dcim.models import Site
 from netbox.constants import EMPTY_TABLE_TEXT
@@ -74,3 +74,21 @@ class SearchViewTestCase(TestCase):
         self.assertHttpStatus(response, 200)
         content = str(response.content)
         self.assertIn(EMPTY_TABLE_TEXT, content)
+
+
+class MediaViewTestCase(TestCase):
+
+    def test_media_login_required(self):
+        url = reverse('media', kwargs={'path': 'foo.txt'})
+        response = Client().get(url)
+
+        # Unauthenticated request should redirect to login page
+        self.assertHttpStatus(response, 302)
+
+    @override_settings(LOGIN_REQUIRED=False)
+    def test_media_login_not_required(self):
+        url = reverse('media', kwargs={'path': 'foo.txt'})
+        response = Client().get(url)
+
+        # Unauthenticated request should return a 404 (not found)
+        self.assertHttpStatus(response, 404)

+ 2 - 3
netbox/netbox/urls.py

@@ -2,7 +2,6 @@ from django.conf import settings
 from django.conf.urls import include
 from django.urls import path
 from django.views.decorators.cache import cache_page
-from django.views.static import serve
 from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
 
 from account.views import LoginView, LogoutView
@@ -10,7 +9,7 @@ from netbox.api.views import APIRootView, StatusView
 from netbox.graphql.schema import schema
 from netbox.graphql.views import NetBoxGraphQLView
 from netbox.plugins.urls import plugin_patterns, plugin_api_patterns
-from netbox.views import HomeView, StaticMediaFailureView, SearchView, htmx
+from netbox.views import HomeView, MediaView, StaticMediaFailureView, SearchView, htmx
 
 _patterns = [
 
@@ -69,7 +68,7 @@ _patterns = [
     path('graphql/', NetBoxGraphQLView.as_view(schema=schema), name='graphql'),
 
     # Serving static media in Django to pipe it through LoginRequiredMiddleware
-    path('media/<path:path>', serve, {'document_root': settings.MEDIA_ROOT}),
+    path('media/<path:path>', MediaView.as_view(), name='media'),
     path('media-failure/', StaticMediaFailureView.as_view(), name='media_failure'),
 
     # Plugins

+ 13 - 6
netbox/netbox/views/generic/bulk_views.py

@@ -541,6 +541,17 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView):
     def get_required_permission(self):
         return get_permission_for_model(self.queryset.model, 'change')
 
+    def post_save_operations(self, form, obj):
+        """
+        This method is called for each object in _update_objects. Override to perform additional object-level
+        operations that are specific to a particular ModelForm.
+        """
+        # Add/remove tags
+        if form.cleaned_data.get('add_tags', None):
+            obj.tags.add(*form.cleaned_data['add_tags'])
+        if form.cleaned_data.get('remove_tags', None):
+            obj.tags.remove(*form.cleaned_data['remove_tags'])
+
     def _update_objects(self, form, request):
         custom_fields = getattr(form, 'custom_fields', {})
         standard_fields = [
@@ -612,11 +623,7 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView):
                 elif form.cleaned_data[name]:
                     getattr(obj, name).set(form.cleaned_data[name])
 
-            # Add/remove tags
-            if form.cleaned_data.get('add_tags', None):
-                obj.tags.add(*form.cleaned_data['add_tags'])
-            if form.cleaned_data.get('remove_tags', None):
-                obj.tags.remove(*form.cleaned_data['remove_tags'])
+            self.post_save_operations(form, obj)
 
         # Rebuild the tree for MPTT models
         if issubclass(self.queryset.model, MPTTModel):
@@ -691,7 +698,7 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView):
                 logger.debug("Form validation failed")
 
         else:
-            form = self.form(request.POST, initial=initial_data)
+            form = self.form(initial=initial_data)
             restrict_form_fields(form, request.user)
 
         # Retrieve objects being edited

+ 10 - 0
netbox/netbox/views/misc.py

@@ -8,6 +8,7 @@ from django.core.cache import cache
 from django.shortcuts import redirect, render
 from django.utils.translation import gettext_lazy as _
 from django.views.generic import View
+from django.views.static import serve
 from django_tables2 import RequestConfig
 from packaging import version
 
@@ -23,6 +24,7 @@ from utilities.views import ConditionalLoginRequiredMixin
 
 __all__ = (
     'HomeView',
+    'MediaView',
     'SearchView',
 )
 
@@ -115,3 +117,11 @@ class SearchView(ConditionalLoginRequiredMixin, View):
             'form': form,
             'table': table,
         })
+
+
+class MediaView(ConditionalLoginRequiredMixin, View):
+    """
+    Wrap Django's serve() view to enforce LOGIN_REQUIRED for static media.
+    """
+    def get(self, request, path):
+        return serve(request, path, document_root=settings.MEDIA_ROOT)

+ 1 - 1
netbox/project-static/package.json

@@ -30,7 +30,7 @@
     "gridstack": "10.3.1",
     "htmx.org": "1.9.12",
     "query-string": "9.1.1",
-    "sass": "1.80.4",
+    "sass": "1.80.5",
     "tom-select": "2.3.1",
     "typeface-inter": "3.18.1",
     "typeface-roboto-mono": "1.1.13"

+ 4 - 4
netbox/project-static/yarn.lock

@@ -2656,10 +2656,10 @@ safe-regex-test@^1.0.3:
     es-errors "^1.3.0"
     is-regex "^1.1.4"
 
-sass@1.80.4:
-  version "1.80.4"
-  resolved "https://registry.yarnpkg.com/sass/-/sass-1.80.4.tgz#bc0418fd796cad2f1a1309d8b4d7fe44b7027de0"
-  integrity sha512-rhMQ2tSF5CsuuspvC94nPM9rToiAFw2h3JTrLlgmNw1MH79v8Cr3DH6KF6o6r+8oofY3iYVPUf66KzC8yuVN1w==
+sass@1.80.5:
+  version "1.80.5"
+  resolved "https://registry.yarnpkg.com/sass/-/sass-1.80.5.tgz#0ba965223d44df22497f2966b498cf5c453fae8f"
+  integrity sha512-TQd2aoQl/+zsxRMEDSxVdpPIqeq9UFc6pr7PzkugiTx3VYCFPUaa3P4RrBQsqok4PO200Vkz0vXQBNlg7W907g==
   dependencies:
     "@parcel/watcher" "^2.4.1"
     chokidar "^4.0.0"

+ 2 - 2
netbox/release.yaml

@@ -1,3 +1,3 @@
-version: "4.1.5"
+version: "4.1.7"
 edition: "Community"
-published: "2024-10-28"
+published: "2024-11-21"

+ 1 - 1
netbox/templates/core/datasource.html

@@ -87,7 +87,7 @@
               {% for name, field in backend.parameters.items %}
                 <tr>
                   <th scope="row">{{ field.label }}</th>
-                  {% if name in backend.sensitive_parameters and not perms.core.change_datasource %}
+                  {% if name in backend.sensitive_parameters %}
                     <td>********</td>
                   {% else %}
                     <td>{{ object.parameters|get_key:name|placeholder }}</td>

+ 2 - 12
netbox/templates/graphiql.html → netbox/templates/graphql/graphiql.html

@@ -1,15 +1,8 @@
+{% load static %}
 {% comment %}
   This template derives from the strawberry-graphql project:
   https://github.com/strawberry-graphql/strawberry/blob/main/strawberry/static/graphiql.html
 {% endcomment %}
-<!--
-The request to this GraphQL server provided the header "Accept: text/html"
-and as a result has been presented GraphiQL - an in-browser IDE for
-exploring GraphQL.
-If you wish to receive JSON, provide the header "Accept: application/json" or
-add "&raw" to the end of the URL within a browser.
--->
-{% load static %}
 <!DOCTYPE html>
 <html>
   <head>
@@ -112,10 +105,7 @@ add "&raw" to the end of the URL within a browser.
         headers["x-csrftoken"] = csrfToken;
       }
 
-      const subscriptionsEnabled = JSON.parse("{{ SUBSCRIPTION_ENABLED }}");
-      const subscriptionUrl = subscriptionsEnabled
-        ? httpUrlToWebSockeUrl(fetchURL)
-        : null;
+      const subscriptionUrl = httpUrlToWebSockeUrl(fetchURL);
 
       const fetcher = GraphiQL.createFetcher({
         url: fetchURL,

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


Разница между файлами не показана из-за своего большого размера
+ 224 - 198
netbox/translations/cs/LC_MESSAGES/django.po


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


Разница между файлами не показана из-за своего большого размера
+ 224 - 198
netbox/translations/da/LC_MESSAGES/django.po


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


Разница между файлами не показана из-за своего большого размера
+ 224 - 198
netbox/translations/de/LC_MESSAGES/django.po


Разница между файлами не показана из-за своего большого размера
+ 225 - 201
netbox/translations/en/LC_MESSAGES/django.po


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


Разница между файлами не показана из-за своего большого размера
+ 224 - 198
netbox/translations/es/LC_MESSAGES/django.po


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


Разница между файлами не показана из-за своего большого размера
+ 224 - 198
netbox/translations/fr/LC_MESSAGES/django.po


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


Разница между файлами не показана из-за своего большого размера
+ 224 - 198
netbox/translations/it/LC_MESSAGES/django.po


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


Разница между файлами не показана из-за своего большого размера
+ 226 - 200
netbox/translations/ja/LC_MESSAGES/django.po


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


Разница между файлами не показана из-за своего большого размера
+ 225 - 198
netbox/translations/nl/LC_MESSAGES/django.po


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


Разница между файлами не показана из-за своего большого размера
+ 224 - 198
netbox/translations/pl/LC_MESSAGES/django.po


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


Разница между файлами не показана из-за своего большого размера
+ 226 - 200
netbox/translations/pt/LC_MESSAGES/django.po


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


Разница между файлами не показана из-за своего большого размера
+ 224 - 198
netbox/translations/ru/LC_MESSAGES/django.po


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


Разница между файлами не показана из-за своего большого размера
+ 224 - 198
netbox/translations/tr/LC_MESSAGES/django.po


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


Разница между файлами не показана из-за своего большого размера
+ 189 - 187
netbox/translations/uk/LC_MESSAGES/django.po


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


Разница между файлами не показана из-за своего большого размера
+ 224 - 198
netbox/translations/zh/LC_MESSAGES/django.po


+ 6 - 6
netbox/utilities/conversion.py

@@ -11,9 +11,9 @@ __all__ = (
 )
 
 
-def to_grams(weight, unit):
+def to_grams(weight, unit) -> int:
     """
-    Convert the given weight to kilograms.
+    Convert the given weight to integer grams.
     """
     try:
         if weight < 0:
@@ -22,13 +22,13 @@ def to_grams(weight, unit):
         raise TypeError(_("Invalid value '{weight}' for weight (must be a number)").format(weight=weight))
 
     if unit == WeightUnitChoices.UNIT_KILOGRAM:
-        return weight * 1000
+        return int(weight * 1000)
     if unit == WeightUnitChoices.UNIT_GRAM:
-        return weight
+        return int(weight)
     if unit == WeightUnitChoices.UNIT_POUND:
-        return weight * Decimal(453.592)
+        return int(weight * Decimal(453.592))
     if unit == WeightUnitChoices.UNIT_OUNCE:
-        return weight * Decimal(28.3495)
+        return int(weight * Decimal(28.3495))
     raise ValueError(
         _("Unknown unit {unit}. Must be one of the following: {valid_units}").format(
             unit=unit,

+ 2 - 2
netbox/virtualization/forms/bulk_edit.py

@@ -153,7 +153,7 @@ class VirtualMachineBulkEditForm(NetBoxModelBulkEditForm):
     )
     disk = forms.IntegerField(
         required=False,
-        label=_('Disk (GB)')
+        label=_('Disk (MB)')
     )
     description = forms.CharField(
         label=_('Description'),
@@ -313,7 +313,7 @@ class VirtualDiskBulkEditForm(NetBoxModelBulkEditForm):
     )
     size = forms.IntegerField(
         required=False,
-        label=_('Size (GB)')
+        label=_('Size (MB)')
     )
     description = forms.CharField(
         label=_('Description'),

+ 1 - 1
netbox/virtualization/forms/filtersets.py

@@ -253,7 +253,7 @@ class VirtualDiskFilterForm(NetBoxModelFilterSetForm):
         label=_('Virtual machine')
     )
     size = forms.IntegerField(
-        label=_('Size (GB)'),
+        label=_('Size (MB)'),
         required=False,
         min_value=1
     )

+ 9 - 9
requirements.txt

@@ -1,5 +1,5 @@
 Django==5.1.2
-django-cors-headers==4.5.0
+django-cors-headers==4.6.0
 django-debug-toolbar==4.4.6
 django-filter==24.3
 django-htmx==1.21.0
@@ -8,31 +8,31 @@ django-mptt==0.16.0
 django-pglocks==1.0.4
 django-prometheus==2.3.1
 django-redis==5.4.0
-django-rich==1.12.0
-django-rq==2.10.2
+django-rich==1.13.0
+django-rq==3.0
 django-taggit==6.1.0
 django-tables2==2.7.0
 django-timezone-field==7.0
 djangorestframework==3.15.2
 drf-spectacular==0.27.2
-drf-spectacular-sidecar==2024.7.1
+drf-spectacular-sidecar==2024.11.1
 feedparser==6.0.11
 gunicorn==23.0.0
 Jinja2==3.1.4
 Markdown==3.7
-mkdocs-material==9.5.42
-mkdocstrings[python-legacy]==0.26.2
+mkdocs-material==9.5.45
+mkdocstrings[python-legacy]==0.27.0
 netaddr==1.3.0
 nh3==0.2.18
 Pillow==11.0.0
 psycopg[c,pool]==3.2.3
 PyYAML==6.0.2
 requests==2.32.3
-rq==1.16.2
+rq==2.0
 social-auth-app-django==5.4.2
 social-auth-core==4.5.4
-strawberry-graphql==0.247.0
-strawberry-graphql-django==0.49.1
+strawberry-graphql==0.251.0
+strawberry-graphql-django==0.50.0
 svgwrite==1.4.3
 tablib==3.7.0
 tzdata==2024.2

Некоторые файлы не были показаны из-за большого количества измененных файлов