mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-13 16:47:34 -06:00
Merge branch 'develop' into feature
This commit is contained in:
commit
f08e36e538
@ -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
|
||||
|
2
.github/ISSUE_TEMPLATE/02-bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/02-bug_report.yaml
vendored
@ -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
.github/ISSUE_TEMPLATE/config.yml
vendored
3
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -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
.github/workflows/ci.yml
vendored
5
.github/workflows/ci.yml
vendored
@ -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
.tx/config
Executable file
12
.tx/config
Executable file
@ -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
|
||||
|
@ -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
|
||||
|
@ -329,6 +329,7 @@
|
||||
"100base-tx",
|
||||
"100base-t1",
|
||||
"1000base-t",
|
||||
"1000base-lx",
|
||||
"1000base-tx",
|
||||
"2.5gbase-t",
|
||||
"5gbase-t",
|
||||
|
@ -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
|
||||
|
||||
|
@ -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:
|
||||
|
||||

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

|
||||
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
|
||||
```
|
||||
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 108 KiB |
Binary file not shown.
Before Width: | Height: | Size: 42 KiB |
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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)'),
|
||||
|
@ -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 = ()
|
||||
|
@ -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 = (
|
||||
|
@ -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(
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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):
|
||||
|
@ -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,
|
||||
|
@ -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,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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -1,3 +1,3 @@
|
||||
version: "4.1.5"
|
||||
version: "4.1.7"
|
||||
edition: "Community"
|
||||
published: "2024-10-28"
|
||||
published: "2024-11-21"
|
||||
|
@ -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>
|
||||
|
@ -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,
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -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,
|
||||
|
@ -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'),
|
||||
|
@ -253,7 +253,7 @@ class VirtualDiskFilterForm(NetBoxModelFilterSetForm):
|
||||
label=_('Virtual machine')
|
||||
)
|
||||
size = forms.IntegerField(
|
||||
label=_('Size (GB)'),
|
||||
label=_('Size (MB)'),
|
||||
required=False,
|
||||
min_value=1
|
||||
)
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user