Compare commits

...

4 Commits

Author SHA1 Message Date
adionit7
b55f36469d Update CodeQL Action from v3 to v4
Some checks are pending
CI / build (20.x, 3.12) (push) Waiting to run
CI / build (20.x, 3.13) (push) Waiting to run
CI / build (20.x, 3.14) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
- Update github/codeql-action/init from @v3 to @v4
- Update github/codeql-action/analyze from @v3 to @v4

Fixes #21156
2026-01-15 16:46:25 -05:00
Martin Hauser
1c46215cd5 feat(extras): Allow updates to data_source and data_file via API
Adds support for PATCHing ConfigContext and ConfigContextProfile with
integer IDs for `data_source` and `data_file`.
Adds regression tests to validate assignment and API functionality.

Fixes #20933
2026-01-15 14:37:16 -05:00
Martin Hauser
0ddc5805c4 fix(core): Use gettext_lazy in data.py
Replace `gettext()` with `gettext_lazy()` to avoid locale-dependent
model serialization (and false-positive pending migration warnings).
Also make a missing `ValidationError` message translatable and
format-safe.

Fixes #21175
2026-01-15 12:47:05 -05:00
github-actions
c1bbc026e2 Update source translation strings
Some checks failed
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
2026-01-15 05:05:36 +00:00
6 changed files with 105 additions and 11 deletions

View File

@@ -30,13 +30,13 @@ jobs:
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
config-file: .github/codeql/codeql-config.yml
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v4
with:
category: "/language:${{matrix.language}}"

View File

@@ -44,3 +44,4 @@ class DataFileSerializer(NetBoxModelSerializer):
'id', 'url', 'display_url', 'display', 'source', 'path', 'last_updated', 'size', 'hash',
]
brief_fields = ('id', 'url', 'display', 'path')
read_only_fields = ['path', 'last_updated', 'size', 'hash']

View File

@@ -12,7 +12,7 @@ from django.core.validators import RegexValidator
from django.db import models
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _
from netbox.constants import CENSOR_TOKEN, CENSOR_TOKEN_CHANGED
from netbox.models import PrimaryModel
@@ -128,7 +128,9 @@ class DataSource(JobsMixin, PrimaryModel):
# Ensure URL scheme matches selected type
if self.backend_class.is_local and self.url_scheme not in ('file', ''):
raise ValidationError({
'source_url': "URLs for local sources must start with file:// (or specify no scheme)"
'source_url': _("URLs for local sources must start with {scheme} (or specify no scheme)").format(
scheme='file://'
)
})
def save(self, *args, **kwargs):

View File

@@ -28,7 +28,7 @@ class ConfigContextProfileSerializer(PrimaryModelSerializer):
)
data_file = DataFileSerializer(
nested=True,
read_only=True
required=False
)
class Meta:
@@ -143,7 +143,7 @@ class ConfigContextSerializer(OwnerMixin, ChangeLogMessageSerializer, ValidatedM
)
data_file = DataFileSerializer(
nested=True,
read_only=True
required=False
)
class Meta:

View File

@@ -1,4 +1,5 @@
import datetime
import hashlib
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse
@@ -7,7 +8,7 @@ from rest_framework import status
from core.choices import ManagedFileRootPathChoices
from core.events import *
from core.models import ObjectType
from core.models import DataFile, DataSource, ObjectType
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, Location, RackRole, Site
from extras.choices import *
from extras.models import *
@@ -731,6 +732,51 @@ class ConfigContextProfileTest(APIViewTestCases.APIViewTestCase):
)
ConfigContextProfile.objects.bulk_create(profiles)
def test_update_data_source_and_data_file(self):
"""
Regression test: Ensure data_source and data_file can be assigned via the API.
This specifically covers PATCHing a ConfigContext with integer IDs for both fields.
"""
self.add_permissions(
'core.view_datafile',
'core.view_datasource',
'extras.view_configcontextprofile',
'extras.change_configcontextprofile',
)
config_context_profile = ConfigContextProfile.objects.first()
# Create a data source and file
datasource = DataSource.objects.create(
name='Data Source 1',
type='local',
source_url='file:///tmp/netbox-datasource/',
)
# Generate a valid dummy YAML file
file_data = b'profile: configcontext\n'
datafile = DataFile.objects.create(
source=datasource,
path='dir1/file1.yml',
last_updated=now(),
size=len(file_data),
hash=hashlib.sha256(file_data).hexdigest(),
data=file_data,
)
url = self._get_detail_url(config_context_profile)
payload = {
'data_source': datasource.pk,
'data_file': datafile.pk,
}
response = self.client.patch(url, payload, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
config_context_profile.refresh_from_db()
self.assertEqual(config_context_profile.data_source_id, datasource.pk)
self.assertEqual(config_context_profile.data_file_id, datafile.pk)
self.assertEqual(response.data['data_source']['id'], datasource.pk)
self.assertEqual(response.data['data_file']['id'], datafile.pk)
class ConfigContextTest(APIViewTestCases.APIViewTestCase):
model = ConfigContext
@@ -812,6 +858,51 @@ class ConfigContextTest(APIViewTestCases.APIViewTestCase):
rendered_context = device.get_config_context()
self.assertEqual(rendered_context['bar'], 456)
def test_update_data_source_and_data_file(self):
"""
Regression test: Ensure data_source and data_file can be assigned via the API.
This specifically covers PATCHing a ConfigContext with integer IDs for both fields.
"""
self.add_permissions(
'core.view_datafile',
'core.view_datasource',
'extras.view_configcontext',
'extras.change_configcontext',
)
config_context = ConfigContext.objects.first()
# Create a data source and file
datasource = DataSource.objects.create(
name='Data Source 1',
type='local',
source_url='file:///tmp/netbox-datasource/',
)
# Generate a valid dummy YAML file
file_data = b'context: config\n'
datafile = DataFile.objects.create(
source=datasource,
path='dir1/file1.yml',
last_updated=now(),
size=len(file_data),
hash=hashlib.sha256(file_data).hexdigest(),
data=file_data,
)
url = self._get_detail_url(config_context)
payload = {
'data_source': datasource.pk,
'data_file': datafile.pk,
}
response = self.client.patch(url, payload, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
config_context.refresh_from_db()
self.assertEqual(config_context.data_source_id, datasource.pk)
self.assertEqual(config_context.data_file_id, datafile.pk)
self.assertEqual(response.data['data_source']['id'], datasource.pk)
self.assertEqual(response.data['data_file']['id'], datafile.pk)
class ConfigTemplateTest(APIViewTestCases.APIViewTestCase):
model = ConfigTemplate

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-13 05:05+0000\n"
"POT-Creation-Date: 2026-01-15 05:05+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -12489,8 +12489,8 @@ msgstr ""
msgid "Delete Selected"
msgstr ""
#: netbox/netbox/plugins/navigation.py:55
#: netbox/netbox/plugins/navigation.py:88
#: netbox/netbox/plugins/navigation.py:53
#: netbox/netbox/plugins/navigation.py:89
msgid "Permissions must be passed as a tuple or list."
msgstr ""
@@ -12498,7 +12498,7 @@ msgstr ""
msgid "Buttons must be passed as a tuple or list."
msgstr ""
#: netbox/netbox/plugins/navigation.py:92
#: netbox/netbox/plugins/navigation.py:95
msgid "Button color must be a choice within ButtonColorChoices."
msgstr ""