diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1d9692194..9d580baa4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,15 +31,15 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} @@ -47,7 +47,7 @@ jobs: run: npm install -g yarn - name: Setup Node.js with Yarn Caching - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} cache: yarn diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index 6019cef5d..a3e66a429 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -14,7 +14,7 @@ jobs: lock: runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v3 + - uses: dessant/lock-threads@v4 with: issue-inactive-days: 90 pr-inactive-days: 30 diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 3b37aae56..22de146a2 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/stale@v6 + - uses: actions/stale@v8 with: close-issue-message: > This issue has been automatically closed due to lack of activity. In an diff --git a/base_requirements.txt b/base_requirements.txt index 4b75b1313..423a9754b 100644 --- a/base_requirements.txt +++ b/base_requirements.txt @@ -120,6 +120,10 @@ psycopg[binary,pool] # https://github.com/yaml/pyyaml/blob/master/CHANGES PyYAML +# Requests +# https://github.com/psf/requests/blob/main/HISTORY.md +requests + # Sentry SDK # https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md sentry-sdk diff --git a/docs/customization/custom-scripts.md b/docs/customization/custom-scripts.md index 3811474d2..0b1ed11df 100644 --- a/docs/customization/custom-scripts.md +++ b/docs/customization/custom-scripts.md @@ -288,7 +288,7 @@ An IPv4 or IPv6 network with a mask. Returns a `netaddr.IPNetwork` object. Two a ## Running Custom Scripts !!! 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.  diff --git a/docs/customization/reports.md b/docs/customization/reports.md index 7e3681304..a821c5da7 100644 --- a/docs/customization/reports.md +++ b/docs/customization/reports.md @@ -132,7 +132,7 @@ Once you have created a report, it will appear in the reports list. Initially, r ## Running Reports !!! 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.  diff --git a/docs/release-notes/version-3.6.md b/docs/release-notes/version-3.6.md index 0a7787e43..6cbcf3e19 100644 --- a/docs/release-notes/version-3.6.md +++ b/docs/release-notes/version-3.6.md @@ -2,6 +2,24 @@ ## 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) diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index 70aceaa49..e41e875e4 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -1192,7 +1192,7 @@ class CableImportForm(NetBoxModelImportForm): termination_object = model.objects.get(device__in=device.virtual_chassis.members.all(), name=name) else: 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") except ObjectDoesNotExist: raise forms.ValidationError(f"{side.upper()} side termination not found: {device} {name}") diff --git a/netbox/dcim/svg/cables.py b/netbox/dcim/svg/cables.py index c01e656fd..acc4fcad9 100644 --- a/netbox/dcim/svg/cables.py +++ b/netbox/dcim/svg/cables.py @@ -160,6 +160,8 @@ class CableTraceSVG: elif instance._meta.model_name == 'circuit': labels[0] = f'Circuit {instance}' labels.append(instance.provider) + if instance.description: + labels.append(instance.description) elif instance._meta.model_name == 'circuittermination': if instance.xconnect_id: labels.append(f'{instance.xconnect_id}') diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index e6f339e5a..2bed464bb 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -232,6 +232,11 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): return self.choice_set.choices 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): """ Populate initial custom field data upon either a) the creation of a new CustomField, or diff --git a/netbox/extras/reports.py b/netbox/extras/reports.py index 9b12065ca..cc279a49a 100644 --- a/netbox/extras/reports.py +++ b/netbox/extras/reports.py @@ -23,7 +23,7 @@ logger = logging.getLogger(__name__) def get_module_and_report(module_name, report_name): 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 diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index da6463e23..643e6a8e0 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -289,7 +289,7 @@ class AvailableObjectsView(ObjectValidationMixin, APIView): ) # 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 serializer_class = get_serializer_for_model(self.queryset.model) diff --git a/netbox/netbox/models/__init__.py b/netbox/netbox/models/__init__.py index 596357ea4..9d7696696 100644 --- a/netbox/netbox/models/__init__.py +++ b/netbox/netbox/models/__init__.py @@ -1,5 +1,6 @@ from django.conf import settings from django.contrib.contenttypes.fields import GenericForeignKey +from django.core.exceptions import ObjectDoesNotExist from django.core.validators import ValidationError from django.db import models from django.utils.translation import gettext_lazy as _ @@ -85,11 +86,16 @@ class NetBoxModel(NetBoxFeatureSet, models.Model): if ct_value and fk_value: 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({ 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 diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 8be2800fb..975e86858 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -355,6 +355,7 @@ INSTALLED_APPS = [ 'django.contrib.messages', 'django.contrib.staticfiles', 'django.contrib.humanize', + 'django.forms', 'corsheaders', '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 if type(REMOTE_AUTH_BACKEND) not in (list, tuple): REMOTE_AUTH_BACKEND = [REMOTE_AUTH_BACKEND] diff --git a/netbox/netbox/tables/columns.py b/netbox/netbox/tables/columns.py index 9e348fb23..d2cd0a0d4 100644 --- a/netbox/netbox/tables/columns.py +++ b/netbox/netbox/tables/columns.py @@ -483,8 +483,10 @@ class CustomFieldColumn(tables.Column): return mark_safe('') if self.customfield.type == CustomFieldTypeChoices.TYPE_URL: return mark_safe(f'{escape(value)}') + if self.customfield.type == CustomFieldTypeChoices.TYPE_SELECT: + return self.customfield.get_choice_label(value) 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: return mark_safe(', '.join( self._linkify_item(obj) for obj in self.customfield.deserialize(value) diff --git a/netbox/templates/dcim/platform.html b/netbox/templates/dcim/platform.html index 29f405b6e..9448ad3e5 100644 --- a/netbox/templates/dcim/platform.html +++ b/netbox/templates/dcim/platform.html @@ -13,7 +13,7 @@ {% block extra_controls %} {% if perms.dcim.add_device %} - + {% trans "Add Device" %} {% endif %} diff --git a/netbox/templates/django/forms/widgets/checkbox.html b/netbox/templates/django/forms/widgets/checkbox.html new file mode 100644 index 000000000..bbe201a29 --- /dev/null +++ b/netbox/templates/django/forms/widgets/checkbox.html @@ -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 %} + +{% include "django/forms/widgets/input.html" %} diff --git a/netbox/templates/generic/bulk_import.html b/netbox/templates/generic/bulk_import.html index b9cb0d4cb..f78cb0bf5 100644 --- a/netbox/templates/generic/bulk_import.html +++ b/netbox/templates/generic/bulk_import.html @@ -67,6 +67,7 @@ Context: {% render_field form.upload_file %} {% render_field form.format %} + {% render_field form.csv_delimiter %}