mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-23 07:56:44 -06:00
Merge branch 'develop' into 12336-serialize-region-api
This commit is contained in:
commit
495b15b291
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@ -31,15 +31,15 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
|
|
||||||
@ -47,7 +47,7 @@ jobs:
|
|||||||
run: npm install -g yarn
|
run: npm install -g yarn
|
||||||
|
|
||||||
- name: Setup Node.js with Yarn Caching
|
- name: Setup Node.js with Yarn Caching
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: yarn
|
cache: yarn
|
||||||
|
2
.github/workflows/lock.yml
vendored
2
.github/workflows/lock.yml
vendored
@ -14,7 +14,7 @@ jobs:
|
|||||||
lock:
|
lock:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: dessant/lock-threads@v3
|
- uses: dessant/lock-threads@v4
|
||||||
with:
|
with:
|
||||||
issue-inactive-days: 90
|
issue-inactive-days: 90
|
||||||
pr-inactive-days: 30
|
pr-inactive-days: 30
|
||||||
|
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@ -15,7 +15,7 @@ jobs:
|
|||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v6
|
- uses: actions/stale@v8
|
||||||
with:
|
with:
|
||||||
close-issue-message: >
|
close-issue-message: >
|
||||||
This issue has been automatically closed due to lack of activity. In an
|
This issue has been automatically closed due to lack of activity. In an
|
||||||
|
@ -120,6 +120,10 @@ psycopg[binary,pool]
|
|||||||
# https://github.com/yaml/pyyaml/blob/master/CHANGES
|
# https://github.com/yaml/pyyaml/blob/master/CHANGES
|
||||||
PyYAML
|
PyYAML
|
||||||
|
|
||||||
|
# Requests
|
||||||
|
# https://github.com/psf/requests/blob/main/HISTORY.md
|
||||||
|
requests
|
||||||
|
|
||||||
# Sentry SDK
|
# Sentry SDK
|
||||||
# https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md
|
# https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md
|
||||||
sentry-sdk
|
sentry-sdk
|
||||||
|
@ -288,7 +288,7 @@ An IPv4 or IPv6 network with a mask. Returns a `netaddr.IPNetwork` object. Two a
|
|||||||
## Running Custom Scripts
|
## Running Custom Scripts
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
To run a custom script, a user must be assigned the `extras.run_script` permission. This is achieved by assigning the user (or group) a permission on the Script object and specifying the `run` action in the admin UI as shown below.
|
To run a custom script, a user must be assigned via permissions for `Extras > Script`, `Extras > ScriptModule`, and `Core > ManagedFile` objects. They must also be assigned the `extras.run_script` permission. This is achieved by assigning the user (or group) a permission on the Script object and specifying the `run` action in the admin UI as shown below.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
@ -132,7 +132,7 @@ Once you have created a report, it will appear in the reports list. Initially, r
|
|||||||
## Running Reports
|
## Running Reports
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
To run a report, a user must be assigned the `extras.run_report` permission. This is achieved by assigning the user (or group) a permission on the Report object and specifying the `run` action in the admin UI as shown below.
|
To run a report, a user must be assigned via permissions for `Extras > Report`, `Extras > ReportModule`, and `Core > ManagedFile` objects. They must also be assigned the `extras.run_report` permission. This is achieved by assigning the user (or group) a permission on the Report object and specifying the `run` action in the admin UI as shown below.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
@ -2,6 +2,24 @@
|
|||||||
|
|
||||||
## v3.6.4 (FUTURE)
|
## v3.6.4 (FUTURE)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#12831](https://github.com/netbox-community/netbox/issues/12831) - Include circuit description in cable trace SVG image
|
||||||
|
* [#13950](https://github.com/netbox-community/netbox/issues/13950) - Display custom choice field labels rather than values in UI
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#11987](https://github.com/netbox-community/netbox/issues/11987) - Fix validation of bulk cable updates via bulk import form
|
||||||
|
* [#12328](https://github.com/netbox-community/netbox/issues/12328) - Ensure generic foreign key relationships are populated in REST API serializations of objects
|
||||||
|
* [#13064](https://github.com/netbox-community/netbox/issues/13064) - Fix resetting of checkbox fields triggered by HTMX form re-rendering
|
||||||
|
* [#13440](https://github.com/netbox-community/netbox/issues/13440) - Fix support for assigning a tenant when creating "next available" VLANs via the REST API
|
||||||
|
* [#13746](https://github.com/netbox-community/netbox/issues/13746) - Fix support for setting custom field values when creating "next available" IP addresses via the REST API
|
||||||
|
* [#13872](https://github.com/netbox-community/netbox/issues/13872) - Add CSV delimiter field to file upload tab under bulk object upload views
|
||||||
|
* [#13876](https://github.com/netbox-community/netbox/issues/13876) - Fix support for assigning an interface when creating "next available" IP addresses via the REST API
|
||||||
|
* [#13910](https://github.com/netbox-community/netbox/issues/13910) - Correct "add device" button link under platform view
|
||||||
|
* [#13944](https://github.com/netbox-community/netbox/issues/13944) - Correct serialization of several report attributes in the REST API
|
||||||
|
* [#13966](https://github.com/netbox-community/netbox/issues/13966) - Restore "last login" column on users table
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## v3.6.3 (2023-09-26)
|
## v3.6.3 (2023-09-26)
|
||||||
|
@ -1192,7 +1192,7 @@ class CableImportForm(NetBoxModelImportForm):
|
|||||||
termination_object = model.objects.get(device__in=device.virtual_chassis.members.all(), name=name)
|
termination_object = model.objects.get(device__in=device.virtual_chassis.members.all(), name=name)
|
||||||
else:
|
else:
|
||||||
termination_object = model.objects.get(device=device, name=name)
|
termination_object = model.objects.get(device=device, name=name)
|
||||||
if termination_object.cable is not None:
|
if termination_object.cable is not None and termination_object.cable != self.instance:
|
||||||
raise forms.ValidationError(f"Side {side.upper()}: {device} {termination_object} is already connected")
|
raise forms.ValidationError(f"Side {side.upper()}: {device} {termination_object} is already connected")
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
raise forms.ValidationError(f"{side.upper()} side termination not found: {device} {name}")
|
raise forms.ValidationError(f"{side.upper()} side termination not found: {device} {name}")
|
||||||
|
@ -160,6 +160,8 @@ class CableTraceSVG:
|
|||||||
elif instance._meta.model_name == 'circuit':
|
elif instance._meta.model_name == 'circuit':
|
||||||
labels[0] = f'Circuit {instance}'
|
labels[0] = f'Circuit {instance}'
|
||||||
labels.append(instance.provider)
|
labels.append(instance.provider)
|
||||||
|
if instance.description:
|
||||||
|
labels.append(instance.description)
|
||||||
elif instance._meta.model_name == 'circuittermination':
|
elif instance._meta.model_name == 'circuittermination':
|
||||||
if instance.xconnect_id:
|
if instance.xconnect_id:
|
||||||
labels.append(f'{instance.xconnect_id}')
|
labels.append(f'{instance.xconnect_id}')
|
||||||
|
@ -232,6 +232,11 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
return self.choice_set.choices
|
return self.choice_set.choices
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
def get_choice_label(self, value):
|
||||||
|
if not hasattr(self, '_choice_map'):
|
||||||
|
self._choice_map = dict(self.choices)
|
||||||
|
return self._choice_map.get(value, value)
|
||||||
|
|
||||||
def populate_initial_data(self, content_types):
|
def populate_initial_data(self, content_types):
|
||||||
"""
|
"""
|
||||||
Populate initial custom field data upon either a) the creation of a new CustomField, or
|
Populate initial custom field data upon either a) the creation of a new CustomField, or
|
||||||
|
@ -23,7 +23,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
def get_module_and_report(module_name, report_name):
|
def get_module_and_report(module_name, report_name):
|
||||||
module = ReportModule.objects.get(file_path=f'{module_name}.py')
|
module = ReportModule.objects.get(file_path=f'{module_name}.py')
|
||||||
report = module.reports.get(report_name)
|
report = module.reports.get(report_name)()
|
||||||
return module, report
|
return module, report
|
||||||
|
|
||||||
|
|
||||||
|
@ -289,7 +289,7 @@ class AvailableObjectsView(ObjectValidationMixin, APIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Prepare object data for deserialization
|
# Prepare object data for deserialization
|
||||||
requested_objects = self.prep_object_data(serializer.validated_data, available_objects, parent)
|
requested_objects = self.prep_object_data(requested_objects, available_objects, parent)
|
||||||
|
|
||||||
# Initialize the serializer with a list or a single object depending on what was requested
|
# Initialize the serializer with a list or a single object depending on what was requested
|
||||||
serializer_class = get_serializer_for_model(self.queryset.model)
|
serializer_class = get_serializer_for_model(self.queryset.model)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.core.validators import ValidationError
|
from django.core.validators import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
@ -85,11 +86,16 @@ class NetBoxModel(NetBoxFeatureSet, models.Model):
|
|||||||
|
|
||||||
if ct_value and fk_value:
|
if ct_value and fk_value:
|
||||||
klass = getattr(self, field.ct_field).model_class()
|
klass = getattr(self, field.ct_field).model_class()
|
||||||
if not klass.objects.filter(pk=fk_value).exists():
|
try:
|
||||||
|
obj = klass.objects.get(pk=fk_value)
|
||||||
|
except ObjectDoesNotExist:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
field.fk_field: f"Related object not found using the provided value: {fk_value}."
|
field.fk_field: f"Related object not found using the provided value: {fk_value}."
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# update the GFK field value
|
||||||
|
setattr(self, field.name, obj)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# NetBox internal base models
|
# NetBox internal base models
|
||||||
|
@ -355,6 +355,7 @@ INSTALLED_APPS = [
|
|||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
'django.contrib.humanize',
|
'django.contrib.humanize',
|
||||||
|
'django.forms',
|
||||||
'corsheaders',
|
'corsheaders',
|
||||||
'debug_toolbar',
|
'debug_toolbar',
|
||||||
'graphiql_debug_toolbar',
|
'graphiql_debug_toolbar',
|
||||||
@ -430,6 +431,9 @@ TEMPLATES = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# This allows us to override Django's stock form widget templates
|
||||||
|
FORM_RENDERER = 'django.forms.renderers.TemplatesSetting'
|
||||||
|
|
||||||
# Set up authentication backends
|
# Set up authentication backends
|
||||||
if type(REMOTE_AUTH_BACKEND) not in (list, tuple):
|
if type(REMOTE_AUTH_BACKEND) not in (list, tuple):
|
||||||
REMOTE_AUTH_BACKEND = [REMOTE_AUTH_BACKEND]
|
REMOTE_AUTH_BACKEND = [REMOTE_AUTH_BACKEND]
|
||||||
|
@ -483,8 +483,10 @@ class CustomFieldColumn(tables.Column):
|
|||||||
return mark_safe('<i class="mdi mdi-close-thick text-danger"></i>')
|
return mark_safe('<i class="mdi mdi-close-thick text-danger"></i>')
|
||||||
if self.customfield.type == CustomFieldTypeChoices.TYPE_URL:
|
if self.customfield.type == CustomFieldTypeChoices.TYPE_URL:
|
||||||
return mark_safe(f'<a href="{escape(value)}">{escape(value)}</a>')
|
return mark_safe(f'<a href="{escape(value)}">{escape(value)}</a>')
|
||||||
|
if self.customfield.type == CustomFieldTypeChoices.TYPE_SELECT:
|
||||||
|
return self.customfield.get_choice_label(value)
|
||||||
if self.customfield.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
|
if self.customfield.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
|
||||||
return ', '.join(v for v in value)
|
return ', '.join(self.customfield.get_choice_label(v) for v in value)
|
||||||
if self.customfield.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
|
if self.customfield.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
|
||||||
return mark_safe(', '.join(
|
return mark_safe(', '.join(
|
||||||
self._linkify_item(obj) for obj in self.customfield.deserialize(value)
|
self._linkify_item(obj) for obj in self.customfield.deserialize(value)
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
{% block extra_controls %}
|
{% block extra_controls %}
|
||||||
{% if perms.dcim.add_device %}
|
{% if perms.dcim.add_device %}
|
||||||
<a href="{% url 'dcim:device_add' %}?role={{ object.pk }}" class="btn btn-sm btn-primary">
|
<a href="{% url 'dcim:device_add' %}?platform={{ object.pk }}" class="btn btn-sm btn-primary">
|
||||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add Device" %}
|
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add Device" %}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
6
netbox/templates/django/forms/widgets/checkbox.html
Normal file
6
netbox/templates/django/forms/widgets/checkbox.html
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{% comment %}
|
||||||
|
Include a hidden field of the same name to ensure that unchecked checkboxes
|
||||||
|
are always included in the submitted form data.
|
||||||
|
{% endcomment %}
|
||||||
|
<input type="hidden" name="{{ widget.name }}" value="">
|
||||||
|
{% include "django/forms/widgets/input.html" %}
|
@ -67,6 +67,7 @@ Context:
|
|||||||
<input type="hidden" name="import_method" value="upload" />
|
<input type="hidden" name="import_method" value="upload" />
|
||||||
{% render_field form.upload_file %}
|
{% render_field form.upload_file %}
|
||||||
{% render_field form.format %}
|
{% render_field form.format %}
|
||||||
|
{% render_field form.csv_delimiter %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col col-md-12 text-end">
|
<div class="col col-md-12 text-end">
|
||||||
<button type="submit" name="file_submit" class="btn btn-primary">{% trans "Submit" %}</button>
|
<button type="submit" name="file_submit" class="btn btn-primary">{% trans "Submit" %}</button>
|
||||||
@ -88,6 +89,7 @@ Context:
|
|||||||
{% render_field form.data_source %}
|
{% render_field form.data_source %}
|
||||||
{% render_field form.data_file %}
|
{% render_field form.data_file %}
|
||||||
{% render_field form.format %}
|
{% render_field form.format %}
|
||||||
|
{% render_field form.csv_delimiter %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col col-md-12 text-end">
|
<div class="col col-md-12 text-end">
|
||||||
<button type="submit" name="file_submit" class="btn btn-primary">{% trans "Submit" %}</button>
|
<button type="submit" name="file_submit" class="btn btn-primary">{% trans "Submit" %}</button>
|
||||||
|
@ -52,7 +52,7 @@ class UserTable(NetBoxTable):
|
|||||||
model = NetBoxUser
|
model = NetBoxUser
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'username', 'first_name', 'last_name', 'email', 'groups', 'is_active', 'is_staff',
|
'pk', 'id', 'username', 'first_name', 'last_name', 'email', 'groups', 'is_active', 'is_staff',
|
||||||
'is_superuser',
|
'is_superuser', 'last_login',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'username', 'first_name', 'last_name', 'email', 'is_active')
|
default_columns = ('pk', 'username', 'first_name', 'last_name', 'email', 'is_active')
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from django import template
|
from django import template
|
||||||
from django.http import QueryDict
|
from django.http import QueryDict
|
||||||
|
|
||||||
|
from extras.choices import CustomFieldTypeChoices
|
||||||
from utilities.utils import dict_to_querydict
|
from utilities.utils import dict_to_querydict
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -38,6 +39,11 @@ def customfield_value(customfield, value):
|
|||||||
customfield: A CustomField instance
|
customfield: A CustomField instance
|
||||||
value: The custom field value applied to an object
|
value: The custom field value applied to an object
|
||||||
"""
|
"""
|
||||||
|
if value:
|
||||||
|
if customfield.type == CustomFieldTypeChoices.TYPE_SELECT:
|
||||||
|
value = customfield.get_choice_label(value)
|
||||||
|
elif customfield.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
|
||||||
|
value = [customfield.get_choice_label(v) for v in value]
|
||||||
return {
|
return {
|
||||||
'customfield': customfield,
|
'customfield': customfield,
|
||||||
'value': value,
|
'value': value,
|
||||||
|
@ -27,6 +27,7 @@ netaddr==0.9.0
|
|||||||
Pillow==10.0.1
|
Pillow==10.0.1
|
||||||
psycopg[binary,pool]==3.1.11
|
psycopg[binary,pool]==3.1.11
|
||||||
PyYAML==6.0.1
|
PyYAML==6.0.1
|
||||||
|
requests==2.28.1
|
||||||
sentry-sdk==1.31.0
|
sentry-sdk==1.31.0
|
||||||
social-auth-app-django==5.3.0
|
social-auth-app-django==5.3.0
|
||||||
social-auth-core[openidconnect]==4.4.2
|
social-auth-core[openidconnect]==4.4.2
|
||||||
|
Loading…
Reference in New Issue
Block a user