Merge branch 'develop' into 16946-graphql-filter-error

This commit is contained in:
Arthur Hanson 2024-08-12 21:44:35 +07:00
commit 28eba4bcce
17 changed files with 316 additions and 82 deletions

View File

@ -58,6 +58,8 @@ intake policy](https://github.com/netbox-community/netbox/wiki/Issue-Intake-Poli
* 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.
* Once you're ready, submit a feature request [using this template](https://github.com/netbox-community/netbox/issues/new?label=type%3A+feature&template=feature_request.yaml). Be sure to provide sufficient context and detail to convey exactly what you're proposing and why. The stronger your use case, the better chance your proposal has of being accepted.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 <a href="{device.get_absolute_url()}">{escape(device)}</a>'
_('Added member <a href="{url}">{escape(device)}</a>').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))

View File

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

View File

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

View File

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

Binary file not shown.

View File

@ -44,3 +44,7 @@ table a {
[data-bs-theme=dark] ::selection {
background-color: rgba(var(--tblr-primary-rgb),.48)
}
pre code {
padding: unset;
}

View File

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

View File

@ -24,7 +24,12 @@
</div>
{% 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 %}

View File

@ -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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\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 <a href=\"{url}\">{escape(device)}</a>"
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 ""

View File

@ -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: [<Region: R1>,<Region: R2>]

View File

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