diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 37797785e..a760b8371 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -40,7 +40,7 @@ NetBox users are welcome to participate in either role, on stage or in the crowd * First, ensure that you're running the [latest stable version](https://github.com/netbox-community/netbox/releases) of NetBox. If you're running an older version, it's likely that the bug has already been fixed. -* Next, search our [issues list](https://github.com/netbox-community/netbox/issues?q=is%3Aissue) to see if the bug you've found has already been reported. If you come across a bug report that seems to match, please click "add a reaction" in the bottom left corner of the issue and add a thumbs up (:thumbsup:). This will help draw more attention to it. Any comments you can add to provide additional information or context would also be much appreciated. +* Next, search our [issues list](https://github.com/netbox-community/netbox/issues?q=is%3Aissue) to see if the bug you've found has already been reported. If you come across a bug report that seems to match, please click "add a reaction" in the bottom left corner of the issue and add a thumbs up ( :thumbsup: ). This will help draw more attention to it. Any comments you can add to provide additional information or context would also be much appreciated. * If you can't find any existing issues (open or closed) that seem to match yours, you're welcome to [submit a new bug report](https://github.com/netbox-community/netbox/issues/new?label=type%3A+bug&template=bug_report.yaml). Be sure to complete the entire report template, including detailed steps that someone triaging your issue can follow to confirm the reported behavior. (If we're not able to replicate the bug based on the information provided, we'll ask for additional detail.) @@ -56,7 +56,9 @@ intake policy](https://github.com/netbox-community/netbox/wiki/Issue-Intake-Poli ## :bulb: Feature Requests -* First, check the GitHub [issues list](https://github.com/netbox-community/netbox/issues?q=is%3Aissue) to see if the feature you have in mind has already been proposed. If you happen to find an open feature request that matches your idea, click "add a reaction" in the top right corner of the issue and add a thumbs up (:thumbsup:). This ensures that the issue has a better chance of receiving attention. Also feel free to add a comment with any additional justification for the feature. +* First, check the GitHub [issues list](https://github.com/netbox-community/netbox/issues?q=is%3Aissue) to see if the feature you have in mind has already been proposed. If you happen to find an open feature request that matches your idea, click "add a reaction" in the top right corner of the issue and add a thumbs up ( :thumbsup: ). This ensures that the issue has a better chance of receiving attention. Also feel free to add a comment with any additional justification for the feature. + +* Please don't submit duplicate issues! Sometimes we reject feature requests, for various reasons. Even if you disagree with those reasons, please **do not** submit a duplicate feature request. It is very disrepectful of the maintainers' time, and you may be barred from opening future issues. * If you have a rough idea that's not quite ready for formal submission yet, start a [GitHub discussion](https://github.com/netbox-community/netbox/discussions) instead. This is a great way to test the viability and narrow down the scope of a new feature prior to submitting a formal proposal, and can serve to generate interest in your idea from other community members. diff --git a/docs/release-notes/version-4.0.md b/docs/release-notes/version-4.0.md index f59fc8793..4afa3eadb 100644 --- a/docs/release-notes/version-4.0.md +++ b/docs/release-notes/version-4.0.md @@ -2,6 +2,13 @@ ## v4.0.9 (FUTURE) +### Bug Fixes + +* [#13459](https://github.com/netbox-community/netbox/issues/13459) - Correct OpenAPI schema type for `TreeNodeMultipleChoiceFilter` +* [#16176](https://github.com/netbox-community/netbox/issues/16176) - Restore ability to select multiple terminating devices when connecting a cable +* [#17038](https://github.com/netbox-community/netbox/issues/17038) - Fix AttributeError exception when attempting to export system status data +* [#17064](https://github.com/netbox-community/netbox/issues/17064) - Fix misaligned text within rendered Markdown code blocks + --- ## v4.0.8 (2024-07-26) diff --git a/netbox/account/views.py b/netbox/account/views.py index d39b00d7a..18c2eb412 100644 --- a/netbox/account/views.py +++ b/netbox/account/views.py @@ -109,7 +109,7 @@ class LoginView(View): # Authenticate user auth_login(request, form.get_user()) logger.info(f"User {request.user} successfully authenticated") - messages.success(request, f"Logged in as {request.user}.") + messages.success(request, _("Logged in as {user}.").format(user=request.user)) # Ensure the user has a UserConfig defined. (This should normally be handled by # create_userconfig() on user creation.) @@ -159,7 +159,7 @@ class LogoutView(View): username = request.user auth_logout(request) logger.info(f"User {username} has logged out") - messages.info(request, "You have logged out.") + messages.info(request, _("You have logged out.")) # Delete session key & language cookies (if set) upon logout response = HttpResponseRedirect(resolve_url(settings.LOGOUT_REDIRECT_URL)) @@ -234,7 +234,7 @@ class ChangePasswordView(LoginRequiredMixin, View): def get(self, request): # LDAP users cannot change their password here if getattr(request.user, 'ldap_username', None): - messages.warning(request, "LDAP-authenticated user credentials cannot be changed within NetBox.") + messages.warning(request, _("LDAP-authenticated user credentials cannot be changed within NetBox.")) return redirect('account:profile') form = PasswordChangeForm(user=request.user) @@ -249,7 +249,7 @@ class ChangePasswordView(LoginRequiredMixin, View): if form.is_valid(): form.save() update_session_auth_hash(request, form.user) - messages.success(request, "Your password has been changed successfully.") + messages.success(request, _("Your password has been changed successfully.")) return redirect('account:profile') return render(request, self.template_name, { diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index b10b83b23..0182e1a93 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -1,6 +1,7 @@ from django.contrib import messages from django.db import transaction from django.shortcuts import get_object_or_404, redirect, render +from django.utils.translation import gettext_lazy as _ from dcim.views import PathTraceView from netbox.views import generic @@ -326,7 +327,9 @@ class CircuitSwapTerminations(generic.ObjectEditView): # Circuit must have at least one termination to swap if not circuit.termination_a and not circuit.termination_z: - messages.error(request, "No terminations have been defined for circuit {}.".format(circuit)) + messages.error(request, _( + "No terminations have been defined for circuit {circuit}." + ).format(circuit=circuit)) return redirect('circuits:circuit', pk=circuit.pk) return render(request, 'circuits/circuit_terminations_swap.html', { @@ -374,7 +377,7 @@ class CircuitSwapTerminations(generic.ObjectEditView): circuit.termination_z = None circuit.save() - messages.success(request, f"Swapped terminations for circuit {circuit}.") + messages.success(request, _("Swapped terminations for circuit {circuit}.").format(circuit=circuit)) return redirect('circuits:circuit', pk=circuit.pk) return render(request, 'circuits/circuit_terminations_swap.html', { diff --git a/netbox/core/views.py b/netbox/core/views.py index 9697a7352..d453cf004 100644 --- a/netbox/core/views.py +++ b/netbox/core/views.py @@ -76,7 +76,10 @@ class DataSourceSyncView(BaseObjectView): datasource = get_object_or_404(self.queryset, pk=pk) job = datasource.enqueue_sync_job(request) - messages.success(request, f"Queued job #{job.pk} to sync {datasource}") + messages.success( + request, + _("Queued job #{id} to sync {datasource}").format(id=job.pk, datasource=datasource) + ) return redirect(datasource.get_absolute_url()) @@ -235,7 +238,7 @@ class ConfigRevisionRestoreView(ContentTypePermissionRequiredMixin, View): candidate_config = get_object_or_404(ConfigRevision, pk=pk) candidate_config.activate() - messages.success(request, f"Restored configuration revision #{pk}") + messages.success(request, _("Restored configuration revision #{id}").format(id=pk)) return redirect(candidate_config.get_absolute_url()) @@ -379,9 +382,9 @@ class BackgroundTaskDeleteView(BaseRQView): # Remove job id from queue and delete the actual job queue.connection.lrem(queue.key, 0, job.id) job.delete() - messages.success(request, f'Deleted job {job_id}') + messages.success(request, _('Job {id} has been deleted.').format(id=job_id)) else: - messages.error(request, f'Error deleting job: {form.errors[0]}') + messages.error(request, _('Error deleting job {id}: {error}').format(id=job_id, error=form.errors[0])) return redirect(reverse('core:background_queue_list')) @@ -394,13 +397,13 @@ class BackgroundTaskRequeueView(BaseRQView): try: job = RQ_Job.fetch(job_id, connection=get_redis_connection(config['connection_config']),) except NoSuchJobError: - raise Http404(_("Job {job_id} not found").format(job_id=job_id)) + raise Http404(_("Job {id} not found.").format(id=job_id)) queue_index = QUEUES_MAP[job.origin] queue = get_queue_by_index(queue_index) requeue_job(job_id, connection=queue.connection, serializer=queue.serializer) - messages.success(request, f'You have successfully requeued: {job_id}') + messages.success(request, _('Job {id} has been re-enqueued.').format(id=job_id)) return redirect(reverse('core:background_task', args=[job_id])) @@ -412,7 +415,7 @@ class BackgroundTaskEnqueueView(BaseRQView): try: job = RQ_Job.fetch(job_id, connection=get_redis_connection(config['connection_config']),) except NoSuchJobError: - raise Http404(_("Job {job_id} not found").format(job_id=job_id)) + raise Http404(_("Job {id} not found.").format(id=job_id)) queue_index = QUEUES_MAP[job.origin] queue = get_queue_by_index(queue_index) @@ -435,7 +438,7 @@ class BackgroundTaskEnqueueView(BaseRQView): registry = ScheduledJobRegistry(queue.name, queue.connection) registry.remove(job) - messages.success(request, f'You have successfully enqueued: {job_id}') + messages.success(request, _('Job {id} has been enqueued.').format(id=job_id)) return redirect(reverse('core:background_task', args=[job_id])) @@ -452,11 +455,11 @@ class BackgroundTaskStopView(BaseRQView): queue_index = QUEUES_MAP[job.origin] queue = get_queue_by_index(queue_index) - stopped, _ = stop_jobs(queue, job_id) - if len(stopped) == 1: - messages.success(request, f'You have successfully stopped {job_id}') + stopped_jobs = stop_jobs(queue, job_id)[0] + if len(stopped_jobs) == 1: + messages.success(request, _('Job {id} has been stopped.').format(id=job_id)) else: - messages.error(request, f'Failed to stop {job_id}') + messages.error(request, _('Failed to stop job {id}').format(id=job_id)) return redirect(reverse('core:background_task', args=[job_id])) @@ -559,13 +562,14 @@ class SystemView(UserPassesTestMixin, View): # Raw data export if 'export' in request.GET: + params = [param.name for param in PARAMS] data = { **stats, 'plugins': { plugin.name: plugin.version for plugin in plugins }, 'config': { - k: config.data[k] for k in sorted(config.data) + k: getattr(config, k) for k in sorted(params) }, } response = HttpResponse(json.dumps(data, indent=4), content_type='text/json') diff --git a/netbox/dcim/forms/connections.py b/netbox/dcim/forms/connections.py index 44bea047a..f107c3476 100644 --- a/netbox/dcim/forms/connections.py +++ b/netbox/dcim/forms/connections.py @@ -19,7 +19,7 @@ def get_cable_form(a_type, b_type): # Device component if hasattr(term_cls, 'device'): - attrs[f'termination_{cable_end}_device'] = DynamicModelChoiceField( + attrs[f'termination_{cable_end}_device'] = DynamicModelMultipleChoiceField( queryset=Device.objects.all(), label=_('Device'), required=False, @@ -33,6 +33,7 @@ def get_cable_form(a_type, b_type): label=term_cls._meta.verbose_name.title(), context={ 'disabled': '_occupied', + 'parent': 'device', }, query_params={ 'device_id': f'$termination_{cable_end}_device', @@ -43,7 +44,7 @@ def get_cable_form(a_type, b_type): # PowerFeed elif term_cls == PowerFeed: - attrs[f'termination_{cable_end}_powerpanel'] = DynamicModelChoiceField( + attrs[f'termination_{cable_end}_powerpanel'] = DynamicModelMultipleChoiceField( queryset=PowerPanel.objects.all(), label=_('Power Panel'), required=False, @@ -57,6 +58,7 @@ def get_cable_form(a_type, b_type): label=_('Power Feed'), context={ 'disabled': '_occupied', + 'parent': 'powerpanel', }, query_params={ 'power_panel_id': f'$termination_{cable_end}_powerpanel', @@ -66,7 +68,7 @@ def get_cable_form(a_type, b_type): # CircuitTermination elif term_cls == CircuitTermination: - attrs[f'termination_{cable_end}_circuit'] = DynamicModelChoiceField( + attrs[f'termination_{cable_end}_circuit'] = DynamicModelMultipleChoiceField( queryset=Circuit.objects.all(), label=_('Circuit'), selector=True, @@ -79,6 +81,7 @@ def get_cable_form(a_type, b_type): label=_('Side'), context={ 'disabled': '_occupied', + 'parent': 'circuit', }, query_params={ 'circuit_id': f'$termination_{cable_end}_circuit', diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index b3d8d298e..b18ecdd5b 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -2059,7 +2059,7 @@ class DeviceRenderConfigView(generic.ObjectView): try: rendered_config = config_template.render(context=context_data) except TemplateError as e: - messages.error(request, f"An error occurred while rendering the template: {e}") + messages.error(request, _("An error occurred while rendering the template: {error}").format(error=e)) rendered_config = traceback.format_exc() return { @@ -2823,7 +2823,13 @@ class DeviceBayPopulateView(generic.ObjectEditView): device_bay.snapshot() device_bay.installed_device = form.cleaned_data['installed_device'] device_bay.save() - messages.success(request, "Added {} to {}.".format(device_bay.installed_device, device_bay)) + messages.success( + request, + _("Installed device {device} in bay {device_bay}.").format( + device=device_bay.installed_device, + device_bay=device_bay + ) + ) return_url = self.get_return_url(request) return redirect(return_url) @@ -2858,7 +2864,13 @@ class DeviceBayDepopulateView(generic.ObjectEditView): removed_device = device_bay.installed_device device_bay.installed_device = None device_bay.save() - messages.success(request, f"{removed_device} has been removed from {device_bay}.") + messages.success( + request, + _("Removed device {device} from bay {device_bay}.").format( + device=removed_device, + device_bay=device_bay + ) + ) return_url = self.get_return_url(request, device_bay.device) return redirect(return_url) @@ -3426,7 +3438,7 @@ class VirtualChassisAddMemberView(ObjectPermissionRequiredMixin, GetReturnURLMix membership_form.save() messages.success(request, mark_safe( - f'Added member {escape(device)}' + _('Added member {escape(device)}').format(url=device.get_absolute_url()) )) if '_addanother' in request.POST: @@ -3471,7 +3483,10 @@ class VirtualChassisRemoveMemberView(ObjectPermissionRequiredMixin, GetReturnURL # Protect master device from being removed virtual_chassis = VirtualChassis.objects.filter(master=device).first() if virtual_chassis is not None: - messages.error(request, f'Unable to remove master device {device} from the virtual chassis.') + messages.error( + request, + _('Unable to remove master device {device} from the virtual chassis.').format(device=device) + ) return redirect(device.get_absolute_url()) if form.is_valid(): @@ -3483,7 +3498,10 @@ class VirtualChassisRemoveMemberView(ObjectPermissionRequiredMixin, GetReturnURL device.vc_priority = None device.save() - msg = 'Removed {} from virtual chassis {}'.format(device, device.virtual_chassis) + msg = _('Removed {device} from virtual chassis {chassis}').format( + device=device, + chassis=device.virtual_chassis + ) messages.success(request, msg) return redirect(self.get_return_url(request, device)) diff --git a/netbox/netbox/tables/tables.py b/netbox/netbox/tables/tables.py index b191896fa..f95263f6c 100644 --- a/netbox/netbox/tables/tables.py +++ b/netbox/netbox/tables/tables.py @@ -6,6 +6,7 @@ from django.contrib.auth.models import AnonymousUser from django.contrib.contenttypes.fields import GenericForeignKey from django.core.exceptions import FieldDoesNotExist from django.db.models.fields.related import RelatedField +from django.db.models.fields.reverse_related import ManyToOneRel from django.urls import reverse from django.urls.exceptions import NoReverseMatch from django.utils.safestring import mark_safe @@ -102,7 +103,7 @@ class BaseTable(tables.Table): field = model._meta.get_field(field_name) except FieldDoesNotExist: break - if isinstance(field, RelatedField): + if isinstance(field, (RelatedField, ManyToOneRel)): # Follow ForeignKeys to the related model prefetch_path.append(field_name) model = field.remote_field.model diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index 71ce411ba..bc6ace885 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -106,7 +106,13 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin): try: return template.render_to_response(self.queryset) except Exception as e: - messages.error(request, f"There was an error rendering the selected export template ({template.name}): {e}") + messages.error( + request, + _("There was an error rendering the selected export template ({template}): {error}").format( + template=template.name, + error=e + ) + ) # Strip the `export` param and redirect user to the filtered objects list query_params = request.GET.copy() query_params.pop('export') @@ -668,7 +674,10 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView): # Retrieve objects being edited table = self.table(self.queryset.filter(pk__in=pk_list), orderable=False) if not table.rows: - messages.warning(request, "No {} were selected.".format(model._meta.verbose_name_plural)) + messages.warning( + request, + _("No {object_type} were selected.").format(object_type=model._meta.verbose_name_plural) + ) return redirect(self.get_return_url(request)) return render(request, self.template_name, { @@ -745,8 +754,13 @@ class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView): if self.queryset.filter(pk__in=renamed_pks).count() != len(selected_objects): raise PermissionsViolation - model_name = self.queryset.model._meta.verbose_name_plural - messages.success(request, f"Renamed {len(selected_objects)} {model_name}") + messages.success( + request, + _("Renamed {count} {object_type}").format( + count=len(selected_objects), + object_type=self.queryset.model._meta.verbose_name_plural + ) + ) return redirect(self.get_return_url(request)) except (AbortRequest, PermissionsViolation) as e: @@ -838,7 +852,10 @@ class BulkDeleteView(GetReturnURLMixin, BaseMultiObjectView): messages.error(request, mark_safe(e.message)) return redirect(self.get_return_url(request)) - msg = f"Deleted {deleted_count} {model._meta.verbose_name_plural}" + msg = _("Deleted {count} {object_type}").format( + count=deleted_count, + object_type=model._meta.verbose_name_plural + ) logger.info(msg) messages.success(request, msg) return redirect(self.get_return_url(request)) @@ -855,7 +872,10 @@ class BulkDeleteView(GetReturnURLMixin, BaseMultiObjectView): # Retrieve objects being deleted table = self.table(self.queryset.filter(pk__in=pk_list), orderable=False) if not table.rows: - messages.warning(request, "No {} were selected for deletion.".format(model._meta.verbose_name_plural)) + messages.warning( + request, + _("No {object_type} were selected.").format(object_type=model._meta.verbose_name_plural) + ) return redirect(self.get_return_url(request)) return render(request, self.template_name, { @@ -900,7 +920,10 @@ class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView): selected_objects = self.parent_model.objects.filter(pk__in=pk_list) if not selected_objects: - messages.warning(request, "No {} were selected.".format(self.parent_model._meta.verbose_name_plural)) + messages.warning( + request, + _("No {object_type} were selected.").format(object_type=self.parent_model._meta.verbose_name_plural) + ) return redirect(self.get_return_url(request)) table = self.table(selected_objects, orderable=False) diff --git a/netbox/netbox/views/generic/feature_views.py b/netbox/netbox/views/generic/feature_views.py index 95b7b5712..240e8ca28 100644 --- a/netbox/netbox/views/generic/feature_views.py +++ b/netbox/netbox/views/generic/feature_views.py @@ -202,11 +202,14 @@ class ObjectSyncDataView(View): obj = get_object_or_404(qs, **kwargs) if not obj.data_file: - messages.error(request, f"Unable to synchronize data: No data file set.") + messages.error(request, _("Unable to synchronize data: No data file set.")) return redirect(obj.get_absolute_url()) obj.sync(save=True) - messages.success(request, f"Synchronized data for {model._meta.verbose_name} {obj}.") + messages.success(request, _("Synchronized data for {object_type} {object}.").format( + object_type=model._meta.verbose_name, + object=obj + )) return redirect(obj.get_absolute_url()) @@ -228,7 +231,9 @@ class BulkSyncDataView(GetReturnURLMixin, BaseMultiObjectView): for obj in selected_objects: obj.sync(save=True) - model_name = self.queryset.model._meta.verbose_name_plural - messages.success(request, f"Synced {len(selected_objects)} {model_name}") + messages.success(request, _("Synced {count} {object_type}").format( + count=len(selected_objects), + object_type=self.queryset.model._meta.verbose_name_plural + )) return redirect(self.get_return_url(request)) diff --git a/netbox/project-static/dist/netbox.css b/netbox/project-static/dist/netbox.css index 36ed4defc..fda6bcf0c 100644 Binary files a/netbox/project-static/dist/netbox.css and b/netbox/project-static/dist/netbox.css differ diff --git a/netbox/project-static/styles/overrides/_tabler.scss b/netbox/project-static/styles/overrides/_tabler.scss index 97f1298df..9ff87e4ef 100644 --- a/netbox/project-static/styles/overrides/_tabler.scss +++ b/netbox/project-static/styles/overrides/_tabler.scss @@ -44,3 +44,7 @@ table a { [data-bs-theme=dark] ::selection { background-color: rgba(var(--tblr-primary-rgb),.48) } + +pre code { + padding: unset; +} diff --git a/netbox/project-static/yarn.lock b/netbox/project-static/yarn.lock index 4f9111c89..bb0bea154 100644 --- a/netbox/project-static/yarn.lock +++ b/netbox/project-static/yarn.lock @@ -867,13 +867,20 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" -braces@^3.0.2, braces@~3.0.2: +braces@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== dependencies: fill-range "^7.0.1" +braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" @@ -1520,10 +1527,10 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== +fill-range@^7.0.1, fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" @@ -1816,9 +1823,9 @@ ignore@^5.2.0: integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== immutable@^4.0.0: - version "4.3.6" - resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.6.tgz#6a05f7858213238e587fb83586ffa3b4b27f0447" - integrity sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ== + version "4.3.7" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.7.tgz#c70145fc90d89fb02021e65c84eb0226e4e5a381" + integrity sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw== import-fresh@^3.2.1: version "3.3.0" diff --git a/netbox/templates/core/rq_worker_list.html b/netbox/templates/core/rq_worker_list.html index d0d8b9149..92ea601ac 100644 --- a/netbox/templates/core/rq_worker_list.html +++ b/netbox/templates/core/rq_worker_list.html @@ -24,7 +24,12 @@ {% endblock page-header %} -{% block title %}{{ status|capfirst }} {% trans "Workers in " %}{{ queue.name }}{% endblock %} +{% block title %} + {{ status|capfirst }} + {% blocktrans trimmed with queue_name=queue.name %} + Workers in {{ queue_name }} + {% endblocktrans %} +{% endblock %} {% block controls %}{% endblock %} diff --git a/netbox/translations/en/LC_MESSAGES/django.po b/netbox/translations/en/LC_MESSAGES/django.po index e37ad9ca6..5a346dc93 100644 --- a/netbox/translations/en/LC_MESSAGES/django.po +++ b/netbox/translations/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-07-27 05:02+0000\n" +"POT-Creation-Date: 2024-08-11 05:02+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -58,10 +58,27 @@ msgstr "" msgid "Allowed IPs" msgstr "" +#: netbox/account/views.py:112 +#, python-brace-format +msgid "Logged in as {user}." +msgstr "" + +#: netbox/account/views.py:162 +msgid "You have logged out." +msgstr "" + #: netbox/account/views.py:214 msgid "Your preferences have been updated." msgstr "" +#: netbox/account/views.py:237 +msgid "LDAP-authenticated user credentials cannot be changed within NetBox." +msgstr "" + +#: netbox/account/views.py:252 +msgid "Your password has been changed successfully." +msgstr "" + #: netbox/circuits/choices.py:21 netbox/dcim/choices.py:20 #: netbox/dcim/choices.py:102 netbox/dcim/choices.py:174 #: netbox/dcim/choices.py:220 netbox/dcim/choices.py:1459 @@ -309,7 +326,7 @@ msgstr "" #: netbox/circuits/forms/filtersets.py:212 #: netbox/circuits/forms/model_forms.py:109 #: netbox/circuits/forms/model_forms.py:131 -#: netbox/circuits/tables/circuits.py:98 netbox/dcim/forms/connections.py:71 +#: netbox/circuits/tables/circuits.py:98 netbox/dcim/forms/connections.py:73 #: netbox/templates/circuits/circuit.html:15 #: netbox/templates/circuits/circuittermination.html:19 #: netbox/templates/dcim/inc/cable_termination.html:55 @@ -532,7 +549,7 @@ msgstr "" #: netbox/dcim/tables/devices.py:802 netbox/dcim/tables/power.py:77 #: netbox/extras/forms/bulk_import.py:39 netbox/extras/tables/tables.py:290 #: netbox/extras/tables/tables.py:362 netbox/extras/tables/tables.py:480 -#: netbox/netbox/tables/tables.py:239 netbox/templates/circuits/circuit.html:30 +#: netbox/netbox/tables/tables.py:240 netbox/templates/circuits/circuit.html:30 #: netbox/templates/core/datasource.html:38 netbox/templates/dcim/cable.html:15 #: netbox/templates/dcim/consoleport.html:36 #: netbox/templates/dcim/consoleserverport.html:36 @@ -964,7 +981,7 @@ msgstr "" #: netbox/ipam/forms/filtersets.py:266 netbox/ipam/forms/filtersets.py:307 #: netbox/ipam/forms/filtersets.py:382 netbox/ipam/forms/filtersets.py:475 #: netbox/ipam/forms/filtersets.py:534 netbox/ipam/forms/filtersets.py:552 -#: netbox/netbox/tables/tables.py:255 +#: netbox/netbox/tables/tables.py:256 #: netbox/virtualization/forms/filtersets.py:45 #: netbox/virtualization/forms/filtersets.py:103 #: netbox/virtualization/forms/filtersets.py:194 @@ -1377,6 +1394,16 @@ msgstr "" msgid "ASN Count" msgstr "" +#: netbox/circuits/views.py:331 +#, python-brace-format +msgid "No terminations have been defined for circuit {circuit}." +msgstr "" + +#: netbox/circuits/views.py:380 +#, python-brace-format +msgid "Swapped terminations for circuit {circuit}." +msgstr "" + #: netbox/core/api/views.py:36 msgid "This user does not have permission to synchronize this data source." msgstr "" @@ -1959,7 +1986,7 @@ msgstr "" #: netbox/core/tables/jobs.py:10 netbox/core/tables/tasks.py:76 #: netbox/dcim/tables/devicetypes.py:165 netbox/extras/tables/tables.py:185 -#: netbox/extras/tables/tables.py:357 netbox/netbox/tables/tables.py:188 +#: netbox/extras/tables/tables.py:357 netbox/netbox/tables/tables.py:189 #: netbox/templates/dcim/virtualchassis_edit.html:52 #: netbox/utilities/forms/forms.py:73 netbox/wireless/tables/wirelesslink.py:16 msgid "ID" @@ -1969,7 +1996,7 @@ msgstr "" #: netbox/extras/tables/tables.py:248 netbox/extras/tables/tables.py:294 #: netbox/extras/tables/tables.py:367 netbox/extras/tables/tables.py:485 #: netbox/extras/tables/tables.py:516 netbox/extras/tables/tables.py:556 -#: netbox/extras/tables/tables.py:593 netbox/netbox/tables/tables.py:243 +#: netbox/extras/tables/tables.py:593 netbox/netbox/tables/tables.py:244 #: netbox/templates/extras/eventrule.html:84 #: netbox/templates/extras/journalentry.html:18 #: netbox/templates/extras/objectchange.html:58 @@ -2007,7 +2034,7 @@ msgstr "" msgid "Oldest Task" msgstr "" -#: netbox/core/tables/tasks.py:42 netbox/templates/core/rq_worker_list.html:34 +#: netbox/core/tables/tasks.py:42 netbox/templates/core/rq_worker_list.html:39 msgid "Workers" msgstr "" @@ -2063,12 +2090,56 @@ msgstr "" msgid "No workers found" msgstr "" -#: netbox/core/views.py:331 netbox/core/views.py:374 netbox/core/views.py:397 -#: netbox/core/views.py:415 netbox/core/views.py:450 +#: netbox/core/views.py:81 +#, python-brace-format +msgid "Queued job #{id} to sync {datasource}" +msgstr "" + +#: netbox/core/views.py:241 +#, python-brace-format +msgid "Restored configuration revision #{id}" +msgstr "" + +#: netbox/core/views.py:334 netbox/core/views.py:377 netbox/core/views.py:453 #, python-brace-format msgid "Job {job_id} not found" msgstr "" +#: netbox/core/views.py:385 +#, python-brace-format +msgid "Job {id} has been deleted." +msgstr "" + +#: netbox/core/views.py:387 +#, python-brace-format +msgid "Error deleting job {id}: {error}" +msgstr "" + +#: netbox/core/views.py:400 netbox/core/views.py:418 +#, python-brace-format +msgid "Job {id} not found." +msgstr "" + +#: netbox/core/views.py:406 +#, python-brace-format +msgid "Job {id} has been re-enqueued." +msgstr "" + +#: netbox/core/views.py:441 +#, python-brace-format +msgid "Job {id} has been enqueued." +msgstr "" + +#: netbox/core/views.py:460 +#, python-brace-format +msgid "Job {id} has been stopped." +msgstr "" + +#: netbox/core/views.py:462 +#, python-brace-format +msgid "Failed to stop job {id}" +msgstr "" + #: netbox/dcim/api/serializers_/devices.py:50 #: netbox/dcim/api/serializers_/devicetypes.py:26 msgid "Position (U)" @@ -4091,7 +4162,7 @@ msgstr "" msgid "A {model} named {name} already exists" msgstr "" -#: netbox/dcim/forms/connections.py:48 netbox/dcim/forms/model_forms.py:686 +#: netbox/dcim/forms/connections.py:49 netbox/dcim/forms/model_forms.py:686 #: netbox/dcim/tables/power.py:66 #: netbox/templates/dcim/inc/cable_termination.html:37 #: netbox/templates/dcim/powerfeed.html:24 @@ -4100,13 +4171,13 @@ msgstr "" msgid "Power Panel" msgstr "" -#: netbox/dcim/forms/connections.py:57 netbox/dcim/forms/model_forms.py:713 +#: netbox/dcim/forms/connections.py:58 netbox/dcim/forms/model_forms.py:713 #: netbox/templates/dcim/powerfeed.html:21 #: netbox/templates/dcim/powerport.html:80 msgid "Power Feed" msgstr "" -#: netbox/dcim/forms/connections.py:79 +#: netbox/dcim/forms/connections.py:81 msgid "Side" msgstr "" @@ -6081,7 +6152,7 @@ msgstr "" #: netbox/templates/virtualization/virtualmachine/base.html:27 #: netbox/templates/virtualization/virtualmachine_list.html:14 #: netbox/virtualization/tables/virtualmachines.py:100 -#: netbox/virtualization/views.py:363 netbox/wireless/tables/wirelesslan.py:55 +#: netbox/virtualization/views.py:365 netbox/wireless/tables/wirelesslan.py:55 msgid "Interfaces" msgstr "" @@ -6381,24 +6452,53 @@ msgstr "" #: netbox/dcim/views.py:2019 netbox/extras/forms/model_forms.py:453 #: netbox/templates/extras/configcontext.html:10 #: netbox/virtualization/forms/model_forms.py:225 -#: netbox/virtualization/views.py:404 +#: netbox/virtualization/views.py:406 msgid "Config Context" msgstr "" -#: netbox/dcim/views.py:2029 netbox/virtualization/views.py:414 +#: netbox/dcim/views.py:2029 netbox/virtualization/views.py:416 msgid "Render Config" msgstr "" +#: netbox/dcim/views.py:2062 netbox/virtualization/views.py:449 +#, python-brace-format +msgid "An error occurred while rendering the template: {error}" +msgstr "" + #: netbox/dcim/views.py:2080 netbox/extras/tables/tables.py:447 #: netbox/netbox/navigation/menu.py:234 netbox/netbox/navigation/menu.py:236 #: netbox/virtualization/views.py:179 msgid "Virtual Machines" msgstr "" -#: netbox/dcim/views.py:2963 netbox/ipam/tables/ip.py:234 +#: netbox/dcim/views.py:2828 +#, python-brace-format +msgid "Installed device {device} in bay {device_bay}." +msgstr "" + +#: netbox/dcim/views.py:2869 +#, python-brace-format +msgid "Removed device {device} from bay {device_bay}." +msgstr "" + +#: netbox/dcim/views.py:2975 netbox/ipam/tables/ip.py:234 msgid "Children" msgstr "" +#: netbox/dcim/views.py:3441 +msgid "Added member {escape(device)}" +msgstr "" + +#: netbox/dcim/views.py:3488 +#, python-brace-format +msgid "Unable to remove master device {device} from the virtual chassis." +msgstr "" + +#: netbox/dcim/views.py:3501 +#, python-brace-format +msgid "Removed {device} from virtual chassis {chassis}" +msgstr "" + #: netbox/extras/api/customfields.py:88 #, python-brace-format msgid "Unknown related object(s): {name}" @@ -10167,7 +10267,7 @@ msgstr "" #: netbox/templates/virtualization/virtualmachine/base.html:32 #: netbox/templates/virtualization/virtualmachine_list.html:21 #: netbox/virtualization/tables/virtualmachines.py:103 -#: netbox/virtualization/views.py:385 +#: netbox/virtualization/views.py:387 msgid "Virtual Disks" msgstr "" @@ -10538,17 +10638,17 @@ msgstr "" msgid "Error" msgstr "" -#: netbox/netbox/tables/tables.py:57 +#: netbox/netbox/tables/tables.py:58 #, python-brace-format msgid "No {model_name} found" msgstr "" -#: netbox/netbox/tables/tables.py:248 +#: netbox/netbox/tables/tables.py:249 #: netbox/templates/generic/bulk_import.html:117 msgid "Field" msgstr "" -#: netbox/netbox/tables/tables.py:251 +#: netbox/netbox/tables/tables.py:252 msgid "Value" msgstr "" @@ -10556,11 +10656,35 @@ msgstr "" msgid "Dummy Plugin" msgstr "" -#: netbox/netbox/views/generic/bulk_views.py:405 +#: netbox/netbox/views/generic/bulk_views.py:111 +#, python-brace-format +msgid "" +"There was an error rendering the selected export template ({template}): " +"{error}" +msgstr "" + +#: netbox/netbox/views/generic/bulk_views.py:411 #, python-brace-format msgid "Row {i}: Object with ID {id} does not exist" msgstr "" +#: netbox/netbox/views/generic/bulk_views.py:679 +#: netbox/netbox/views/generic/bulk_views.py:877 +#: netbox/netbox/views/generic/bulk_views.py:925 +#, python-brace-format +msgid "No {object_type} were selected." +msgstr "" + +#: netbox/netbox/views/generic/bulk_views.py:759 +#, python-brace-format +msgid "Renamed {count} {object_type}" +msgstr "" + +#: netbox/netbox/views/generic/bulk_views.py:855 +#, python-brace-format +msgid "Deleted {count} {object_type}" +msgstr "" + #: netbox/netbox/views/generic/feature_views.py:38 msgid "Changelog" msgstr "" @@ -10569,6 +10693,20 @@ msgstr "" msgid "Journal" msgstr "" +#: netbox/netbox/views/generic/feature_views.py:205 +msgid "Unable to synchronize data: No data file set." +msgstr "" + +#: netbox/netbox/views/generic/feature_views.py:209 +#, python-brace-format +msgid "Synchronized data for {object_type} {object}." +msgstr "" + +#: netbox/netbox/views/generic/feature_views.py:234 +#, python-brace-format +msgid "Synced {count} {object_type}" +msgstr "" + #: netbox/netbox/views/generic/object_views.py:108 #, python-brace-format msgid "{class_name} must implement get_children()" @@ -11128,8 +11266,8 @@ msgstr "" #: netbox/templates/core/rq_queue_list.html:24 #: netbox/templates/core/rq_queue_list.html:25 -#: netbox/templates/core/rq_worker_list.html:44 -#: netbox/templates/core/rq_worker_list.html:45 +#: netbox/templates/core/rq_worker_list.html:49 +#: netbox/templates/core/rq_worker_list.html:50 #: netbox/templates/extras/script_result.html:49 #: netbox/templates/extras/script_result.html:51 #: netbox/templates/inc/table_controls_htmx.html:30 @@ -11234,8 +11372,9 @@ msgstr "" msgid "Background Workers" msgstr "" -#: netbox/templates/core/rq_worker_list.html:27 -msgid "Workers in " +#: netbox/templates/core/rq_worker_list.html:29 +#, python-format +msgid "Workers in %(queue_name)s" msgstr "" #: netbox/templates/core/system.html:11 @@ -14334,6 +14473,16 @@ msgstr "" msgid "virtual disks" msgstr "" +#: netbox/virtualization/views.py:274 +#, python-brace-format +msgid "Added {count} devices to cluster {cluster}" +msgstr "" + +#: netbox/virtualization/views.py:309 +#, python-brace-format +msgid "Removed {count} devices from cluster {cluster}" +msgstr "" + #: netbox/vpn/choices.py:31 msgid "IPsec - Transport" msgstr "" diff --git a/netbox/utilities/filters.py b/netbox/utilities/filters.py index 7bbc4a6a2..05454543e 100644 --- a/netbox/utilities/filters.py +++ b/netbox/utilities/filters.py @@ -3,8 +3,8 @@ from django import forms from django.conf import settings from django.core.exceptions import ValidationError from django_filters.constants import EMPTY_VALUES -from drf_spectacular.utils import extend_schema_field from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import extend_schema_field __all__ = ( 'ContentTypeFilter', @@ -116,6 +116,7 @@ class MultiValueWWNFilter(django_filters.MultipleChoiceFilter): field_class = multivalue_field_factory(forms.CharField) +@extend_schema_field(OpenApiTypes.STR) class TreeNodeMultipleChoiceFilter(django_filters.ModelMultipleChoiceFilter): """ Filters for a set of Models, including all descendant models within a Tree. Example: [,] diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index 1030fed04..7346f6b0b 100644 --- a/netbox/virtualization/views.py +++ b/netbox/virtualization/views.py @@ -271,8 +271,9 @@ class ClusterAddDevicesView(generic.ObjectEditView): device.cluster = cluster device.save() - messages.success(request, "Added {} devices to cluster {}".format( - len(device_pks), cluster + messages.success(request, _("Added {count} devices to cluster {cluster}").format( + count=len(device_pks), + cluster=cluster )) return redirect(cluster.get_absolute_url()) @@ -305,8 +306,9 @@ class ClusterRemoveDevicesView(generic.ObjectEditView): device.cluster = None device.save() - messages.success(request, "Removed {} devices from cluster {}".format( - len(device_pks), cluster + messages.success(request, _("Removed {count} devices from cluster {cluster}").format( + count=len(device_pks), + cluster=cluster )) return redirect(cluster.get_absolute_url()) @@ -444,7 +446,7 @@ class VirtualMachineRenderConfigView(generic.ObjectView): try: rendered_config = config_template.render(context=context_data) except TemplateError as e: - messages.error(request, f"An error occurred while rendering the template: {e}") + messages.error(request, _("An error occurred while rendering the template: {error}").format(error=e)) rendered_config = traceback.format_exc() return {