Merge branch 'develop' into 10769-cable-delete

This commit is contained in:
Arthur 2023-10-13 07:54:55 -07:00
commit edae010837
23 changed files with 95 additions and 29 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -80,6 +80,14 @@ changes in the database indefinitely.
--- ---
## DATA_UPLOAD_MAX_MEMORY_SIZE
Default: `2621440` (2.5 MB)
The maximum size (in bytes) of an incoming HTTP request (i.e. `GET` or `POST` data). Requests which exceed this size will raise a `RequestDataTooBig` exception.
---
## ENFORCE_GLOBAL_UNIQUE ## ENFORCE_GLOBAL_UNIQUE
!!! tip "Dynamic Configuration Parameter" !!! tip "Dynamic Configuration Parameter"
@ -90,9 +98,9 @@ By default, NetBox will permit users to create duplicate prefixes and IP address
--- ---
## `FILE_UPLOAD_MAX_MEMORY_SIZE` ## FILE_UPLOAD_MAX_MEMORY_SIZE
Default: `2621440` (2.5 MB). Default: `2621440` (2.5 MB)
The maximum amount (in bytes) of uploaded data that will be held in memory before being written to the filesystem. Changing this setting can be useful for example to be able to upload files bigger than 2.5MB to custom scripts for processing. The maximum amount (in bytes) of uploaded data that will be held in memory before being written to the filesystem. Changing this setting can be useful for example to be able to upload files bigger than 2.5MB to custom scripts for processing.

View File

@ -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.
![Adding the run action to a permission](../media/admin_ui_run_permission.png) ![Adding the run action to a permission](../media/admin_ui_run_permission.png)

View File

@ -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.
![Adding the run action to a permission](../media/admin_ui_run_permission.png) ![Adding the run action to a permission](../media/admin_ui_run_permission.png)

View File

@ -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)

View File

@ -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}")

View File

@ -109,7 +109,7 @@ class DeviceComponentFilterForm(NetBoxModelFilterSetForm):
required=False, required=False,
label=_('Device type') label=_('Device type')
) )
role_id = DynamicModelMultipleChoiceField( device_role_id = DynamicModelMultipleChoiceField(
queryset=DeviceRole.objects.all(), queryset=DeviceRole.objects.all(),
required=False, required=False,
label=_('Device role') label=_('Device role')
@ -1150,7 +1150,7 @@ class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
(_('Attributes'), ('name', 'label', 'type', 'speed')), (_('Attributes'), ('name', 'label', 'type', 'speed')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')), (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
(_('Device'), ('device_type_id', 'role_id', 'device_id', 'virtual_chassis_id')), (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
(_('Connection'), ('cabled', 'connected', 'occupied')), (_('Connection'), ('cabled', 'connected', 'occupied')),
) )
type = forms.MultipleChoiceField( type = forms.MultipleChoiceField(
@ -1172,7 +1172,7 @@ class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterF
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
(_('Attributes'), ('name', 'label', 'type', 'speed')), (_('Attributes'), ('name', 'label', 'type', 'speed')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')), (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
(_('Device'), ('device_type_id', 'role_id', 'device_id', 'virtual_chassis_id')), (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
(_('Connection'), ('cabled', 'connected', 'occupied')), (_('Connection'), ('cabled', 'connected', 'occupied')),
) )
type = forms.MultipleChoiceField( type = forms.MultipleChoiceField(
@ -1194,7 +1194,7 @@ class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
(_('Attributes'), ('name', 'label', 'type')), (_('Attributes'), ('name', 'label', 'type')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')), (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
(_('Device'), ('device_type_id', 'role_id', 'device_id', 'virtual_chassis_id')), (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
(_('Connection'), ('cabled', 'connected', 'occupied')), (_('Connection'), ('cabled', 'connected', 'occupied')),
) )
type = forms.MultipleChoiceField( type = forms.MultipleChoiceField(
@ -1211,7 +1211,7 @@ class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
(_('Attributes'), ('name', 'label', 'type')), (_('Attributes'), ('name', 'label', 'type')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')), (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
(_('Device'), ('device_type_id', 'role_id', 'device_id', 'virtual_chassis_id')), (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
(_('Connection'), ('cabled', 'connected', 'occupied')), (_('Connection'), ('cabled', 'connected', 'occupied')),
) )
type = forms.MultipleChoiceField( type = forms.MultipleChoiceField(
@ -1231,7 +1231,7 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
(_('PoE'), ('poe_mode', 'poe_type')), (_('PoE'), ('poe_mode', 'poe_type')),
(_('Wireless'), ('rf_role', 'rf_channel', 'rf_channel_width', 'tx_power')), (_('Wireless'), ('rf_role', 'rf_channel', 'rf_channel_width', 'tx_power')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')), (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
(_('Device'), ('device_type_id', 'role_id', 'device_id', 'virtual_chassis_id', 'vdc_id')), (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', 'vdc_id')),
(_('Connection'), ('cabled', 'connected', 'occupied')), (_('Connection'), ('cabled', 'connected', 'occupied')),
) )
vdc_id = DynamicModelMultipleChoiceField( vdc_id = DynamicModelMultipleChoiceField(
@ -1338,7 +1338,7 @@ class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
(_('Attributes'), ('name', 'label', 'type', 'color')), (_('Attributes'), ('name', 'label', 'type', 'color')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')), (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
(_('Device'), ('device_type_id', 'role_id', 'device_id', 'virtual_chassis_id')), (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
(_('Cable'), ('cabled', 'occupied')), (_('Cable'), ('cabled', 'occupied')),
) )
model = FrontPort model = FrontPort
@ -1360,7 +1360,7 @@ class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
(_('Attributes'), ('name', 'label', 'type', 'color')), (_('Attributes'), ('name', 'label', 'type', 'color')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')), (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
(_('Device'), ('device_type_id', 'role_id', 'device_id', 'virtual_chassis_id')), (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
(_('Cable'), ('cabled', 'occupied')), (_('Cable'), ('cabled', 'occupied')),
) )
type = forms.MultipleChoiceField( type = forms.MultipleChoiceField(
@ -1381,7 +1381,7 @@ class ModuleBayFilterForm(DeviceComponentFilterForm):
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
(_('Attributes'), ('name', 'label', 'position')), (_('Attributes'), ('name', 'label', 'position')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')), (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
(_('Device'), ('device_type_id', 'role_id', 'device_id', 'virtual_chassis_id')), (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
) )
tag = TagFilterField(model) tag = TagFilterField(model)
position = forms.CharField( position = forms.CharField(
@ -1396,7 +1396,7 @@ class DeviceBayFilterForm(DeviceComponentFilterForm):
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
(_('Attributes'), ('name', 'label')), (_('Attributes'), ('name', 'label')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')), (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
(_('Device'), ('device_type_id', 'role_id', 'device_id', 'virtual_chassis_id')), (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
) )
tag = TagFilterField(model) tag = TagFilterField(model)
@ -1407,7 +1407,7 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
(_('Attributes'), ('name', 'label', 'role_id', 'manufacturer_id', 'serial', 'asset_tag', 'discovered')), (_('Attributes'), ('name', 'label', 'role_id', 'manufacturer_id', 'serial', 'asset_tag', 'discovered')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')), (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
(_('Device'), ('device_type_id', 'role_id', 'device_id', 'virtual_chassis_id')), (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
) )
role_id = DynamicModelMultipleChoiceField( role_id = DynamicModelMultipleChoiceField(
queryset=InventoryItemRole.objects.all(), queryset=InventoryItemRole.objects.all(),

View File

@ -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}')

View File

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

View File

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

View File

@ -266,6 +266,7 @@ class AvailableObjectsView(ObjectValidationMixin, APIView):
# Normalize request data to a list of objects # Normalize request data to a list of objects
requested_objects = request.data if isinstance(request.data, list) else [request.data] requested_objects = request.data if isinstance(request.data, list) else [request.data]
limit = len(requested_objects)
# Serialize and validate the request data # Serialize and validate the request data
serializer = self.write_serializer_class(data=requested_objects, many=True, context={ serializer = self.write_serializer_class(data=requested_objects, many=True, context={
@ -279,7 +280,7 @@ class AvailableObjectsView(ObjectValidationMixin, APIView):
) )
with advisory_lock(ADVISORY_LOCK_KEYS[self.advisory_lock_key]): with advisory_lock(ADVISORY_LOCK_KEYS[self.advisory_lock_key]):
available_objects = self.get_available_objects(parent) available_objects = self.get_available_objects(parent, limit)
# Determine if the requested number of objects is available # Determine if the requested number of objects is available
if not self.check_sufficient_available(serializer.validated_data, available_objects): if not self.check_sufficient_available(serializer.validated_data, available_objects):
@ -289,7 +290,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)

View File

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

View File

@ -95,6 +95,7 @@ CORS_ORIGIN_WHITELIST = getattr(configuration, 'CORS_ORIGIN_WHITELIST', [])
CSRF_COOKIE_NAME = getattr(configuration, 'CSRF_COOKIE_NAME', 'csrftoken') CSRF_COOKIE_NAME = getattr(configuration, 'CSRF_COOKIE_NAME', 'csrftoken')
CSRF_COOKIE_SECURE = getattr(configuration, 'CSRF_COOKIE_SECURE', False) CSRF_COOKIE_SECURE = getattr(configuration, 'CSRF_COOKIE_SECURE', False)
CSRF_TRUSTED_ORIGINS = getattr(configuration, 'CSRF_TRUSTED_ORIGINS', []) CSRF_TRUSTED_ORIGINS = getattr(configuration, 'CSRF_TRUSTED_ORIGINS', [])
DATA_UPLOAD_MAX_MEMORY_SIZE = getattr(configuration, 'DATA_UPLOAD_MAX_MEMORY_SIZE', 2621440)
DATE_FORMAT = getattr(configuration, 'DATE_FORMAT', 'N j, Y') DATE_FORMAT = getattr(configuration, 'DATE_FORMAT', 'N j, Y')
DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a') DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a')
DEBUG = getattr(configuration, 'DEBUG', False) DEBUG = getattr(configuration, 'DEBUG', False)
@ -355,6 +356,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 +432,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]

View File

@ -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)

View File

@ -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 %}

View 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" %}

View File

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

View File

@ -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')

View File

@ -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,

View File

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