diff --git a/netbox/circuits/models/circuits.py b/netbox/circuits/models/circuits.py index 4dc775364..7b65d52ad 100644 --- a/netbox/circuits/models/circuits.py +++ b/netbox/circuits/models/circuits.py @@ -234,9 +234,9 @@ class CircuitTermination( # Must define either site *or* provider network if self.site is None and self.provider_network is None: - raise ValidationError("A circuit termination must attach to either a site or a provider network.") + raise ValidationError(_("A circuit termination must attach to either a site or a provider network.")) if self.site and self.provider_network: - raise ValidationError("A circuit termination cannot attach to both a site and a provider network.") + raise ValidationError(_("A circuit termination cannot attach to both a site and a provider network.")) def to_objectchange(self, action): objectchange = super().to_objectchange(action) diff --git a/netbox/core/data_backends.py b/netbox/core/data_backends.py index 9ff0b4d63..15891a6f5 100644 --- a/netbox/core/data_backends.py +++ b/netbox/core/data_backends.py @@ -102,7 +102,7 @@ class GitBackend(DataBackend): try: porcelain.clone(self.url, local_path.name, **clone_args) except BaseException as e: - raise SyncError(f"Fetching remote data failed ({type(e).__name__}): {e}") + raise SyncError(_("Fetching remote data failed ({name}): {error}").format(name=type(e).__name__, error=e)) yield local_path.name diff --git a/netbox/core/forms/model_forms.py b/netbox/core/forms/model_forms.py index 652728734..ae891dd59 100644 --- a/netbox/core/forms/model_forms.py +++ b/netbox/core/forms/model_forms.py @@ -103,9 +103,9 @@ class ManagedFileForm(SyncedDataMixin, NetBoxModelForm): super().clean() if self.cleaned_data.get('upload_file') and self.cleaned_data.get('data_file'): - raise forms.ValidationError("Cannot upload a file and sync from an existing file") + raise forms.ValidationError(_("Cannot upload a file and sync from an existing file")) if not self.cleaned_data.get('upload_file') and not self.cleaned_data.get('data_file'): - raise forms.ValidationError("Must upload a file or select a data file to sync") + raise forms.ValidationError(_("Must upload a file or select a data file to sync")) return self.cleaned_data diff --git a/netbox/core/models/data.py b/netbox/core/models/data.py index 6597a4b4d..4ceb22ba9 100644 --- a/netbox/core/models/data.py +++ b/netbox/core/models/data.py @@ -177,7 +177,7 @@ class DataSource(JobsMixin, PrimaryModel): Create/update/delete child DataFiles as necessary to synchronize with the remote source. """ if self.status == DataSourceStatusChoices.SYNCING: - raise SyncError("Cannot initiate sync; syncing already in progress.") + raise SyncError(_("Cannot initiate sync; syncing already in progress.")) # Emit the pre_sync signal pre_sync.send(sender=self.__class__, instance=self) @@ -190,7 +190,7 @@ class DataSource(JobsMixin, PrimaryModel): backend = self.get_backend() except ModuleNotFoundError as e: raise SyncError( - f"There was an error initializing the backend. A dependency needs to be installed: {e}" + _("There was an error initializing the backend. A dependency needs to be installed: ") + str(e) ) with backend.fetch() as local_path: diff --git a/netbox/core/models/jobs.py b/netbox/core/models/jobs.py index 7cc62a15a..2e3425129 100644 --- a/netbox/core/models/jobs.py +++ b/netbox/core/models/jobs.py @@ -181,7 +181,11 @@ class Job(models.Model): """ valid_statuses = JobStatusChoices.TERMINAL_STATE_CHOICES if status not in valid_statuses: - raise ValueError(f"Invalid status for job termination. Choices are: {', '.join(valid_statuses)}") + raise ValueError( + _("Invalid status for job termination. Choices are: {choices}").format( + choices=', '.join(valid_statuses) + ) + ) # Mark the job as completed self.status = status diff --git a/netbox/dcim/fields.py b/netbox/dcim/fields.py index db1a28d39..535756bac 100644 --- a/netbox/dcim/fields.py +++ b/netbox/dcim/fields.py @@ -1,6 +1,7 @@ from django.contrib.postgres.fields import ArrayField from django.core.exceptions import ValidationError from django.db import models +from django.utils.translation import gettext as _ from netaddr import AddrFormatError, EUI, eui64_unix_expanded, mac_unix_expanded from .lookups import PathContains @@ -41,7 +42,7 @@ class MACAddressField(models.Field): try: return EUI(value, version=48, dialect=mac_unix_expanded_uppercase) except AddrFormatError: - raise ValidationError(f"Invalid MAC address format: {value}") + raise ValidationError(_("Invalid MAC address format: {value}").format(value=value)) def db_type(self, connection): return 'macaddr' @@ -67,7 +68,7 @@ class WWNField(models.Field): try: return EUI(value, version=64, dialect=eui64_unix_expanded_uppercase) except AddrFormatError: - raise ValidationError(f"Invalid WWN format: {value}") + raise ValidationError(_("Invalid WWN format: {value}").format(value=value)) def db_type(self, connection): return 'macaddr8' diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index 732bb87ae..bb68bbeec 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -870,7 +870,11 @@ class InterfaceImportForm(NetBoxModelImportForm): def clean_vdcs(self): for vdc in self.cleaned_data['vdcs']: if vdc.device != self.cleaned_data['device']: - raise forms.ValidationError(f"VDC {vdc} is not assigned to device {self.cleaned_data['device']}") + raise forms.ValidationError( + _("VDC {vdc} is not assigned to device {device}").format( + vdc=vdc, device=self.cleaned_data['device'] + ) + ) return self.cleaned_data['vdcs'] @@ -1075,7 +1079,11 @@ class InventoryItemImportForm(NetBoxModelImportForm): component = model.objects.get(device=device, name=component_name) self.instance.component = component except ObjectDoesNotExist: - raise forms.ValidationError(f"Component not found: {device} - {component_name}") + raise forms.ValidationError( + _("Component not found: {device} - {component_name}").format( + device=device, component_name=component_name + ) + ) # @@ -1193,10 +1201,17 @@ class CableImportForm(NetBoxModelImportForm): else: termination_object = model.objects.get(device=device, name=name) 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( + _("Side {side_upper}: {device} {termination_object} is already connected").format( + side_upper=side.upper(), device=device, termination_object=termination_object + ) + ) except ObjectDoesNotExist: - raise forms.ValidationError(f"{side.upper()} side termination not found: {device} {name}") - + raise forms.ValidationError( + _("{side_upper} side termination not found: {device} {name}").format( + side_upper=side.upper(), device=device, name=name + ) + ) setattr(self.instance, f'{side}_terminations', [termination_object]) return termination_object diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py index d1c80d0be..464735125 100644 --- a/netbox/dcim/models/cables.py +++ b/netbox/dcim/models/cables.py @@ -160,25 +160,26 @@ class Cable(PrimaryModel): # Validate length and length_unit if self.length is not None and not self.length_unit: - raise ValidationError("Must specify a unit when setting a cable length") + raise ValidationError(_("Must specify a unit when setting a cable length")) if self.pk is None and (not self.a_terminations or not self.b_terminations): - raise ValidationError("Must define A and B terminations when creating a new cable.") + raise ValidationError(_("Must define A and B terminations when creating a new cable.")) if self._terminations_modified: # Check that all termination objects for either end are of the same type for terms in (self.a_terminations, self.b_terminations): if len(terms) > 1 and not all(isinstance(t, type(terms[0])) for t in terms[1:]): - raise ValidationError("Cannot connect different termination types to same end of cable.") + raise ValidationError(_("Cannot connect different termination types to same end of cable.")) # Check that termination types are compatible if self.a_terminations and self.b_terminations: a_type = self.a_terminations[0]._meta.model_name b_type = self.b_terminations[0]._meta.model_name if b_type not in COMPATIBLE_TERMINATION_TYPES.get(a_type): - raise ValidationError(f"Incompatible termination types: {a_type} and {b_type}") - + raise ValidationError( + _("Incompatible termination types: {type_a} and {type_b}").format(type_a=a_type, type_b=b_type) + ) if a_type == b_type: # can't directly use self.a_terminations here as possible they # don't have pk yet @@ -323,17 +324,24 @@ class CableTermination(ChangeLoggedModel): ).first() if existing_termination is not None: raise ValidationError( - f"Duplicate termination found for {self.termination_type.app_label}.{self.termination_type.model} " - f"{self.termination_id}: cable {existing_termination.cable.pk}" + _("Duplicate termination found for {app_label}.{model} {termination_id}: cable {cable_pk}".format( + app_label=self.termination_type.app_label, + model=self.termination_type.model, + termination_id=self.termination_id, + cable_pk=existing_termination.cable.pk + )) ) - # Validate interface type (if applicable) if self.termination_type.model == 'interface' and self.termination.type in NONCONNECTABLE_IFACE_TYPES: - raise ValidationError(f"Cables cannot be terminated to {self.termination.get_type_display()} interfaces") + raise ValidationError( + _("Cables cannot be terminated to {type_display} interfaces").format( + type_display=self.termination.get_type_display() + ) + ) # A CircuitTermination attached to a ProviderNetwork cannot have a Cable if self.termination_type.model == 'circuittermination' and self.termination.provider_network is not None: - raise ValidationError("Circuit terminations attached to a provider network may not be cabled.") + raise ValidationError(_("Circuit terminations attached to a provider network may not be cabled.")) def save(self, *args, **kwargs): diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index f36b11033..d02422c6f 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -1,6 +1,7 @@ from django.contrib.auth import get_user_model from django.test import override_settings from django.urls import reverse +from django.utils.translation import gettext as _ from rest_framework import status from dcim.choices import * @@ -45,7 +46,7 @@ class Mixins: name='Peer Device' ) if self.peer_termination_type is None: - raise NotImplementedError("Test case must set peer_termination_type") + raise NotImplementedError(_("Test case must set peer_termination_type")) peer_obj = self.peer_termination_type.objects.create( device=peer_device, name='Peer Termination' diff --git a/netbox/extras/api/customfields.py b/netbox/extras/api/customfields.py index ce3b388f4..6cd3a245e 100644 --- a/netbox/extras/api/customfields.py +++ b/netbox/extras/api/customfields.py @@ -1,4 +1,5 @@ from django.contrib.contenttypes.models import ContentType +from django.utils.translation import gettext as _ from drf_spectacular.utils import extend_schema_field from drf_spectacular.types import OpenApiTypes from rest_framework.fields import Field @@ -88,7 +89,7 @@ class CustomFieldsDataField(Field): if serializer.is_valid(): data[cf.name] = [obj['id'] for obj in serializer.data] if many else serializer.data['id'] else: - raise ValidationError(f"Unknown related object(s): {data[cf.name]}") + raise ValidationError(_("Unknown related object(s): {name}").format(name=data[cf.name])) # If updating an existing instance, start with existing custom_field_data if self.parent.instance: diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 714c92548..8f00e11d9 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -1,5 +1,6 @@ from django.contrib.auth import get_user_model from django.core.exceptions import ObjectDoesNotExist +from django.utils.translation import gettext as _ from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import extend_schema_field from rest_framework import serializers @@ -150,7 +151,7 @@ class CustomFieldSerializer(ValidatedModelSerializer): def validate_type(self, value): if self.instance and self.instance.type != value: - raise serializers.ValidationError('Changing the type of custom fields is not supported.') + raise serializers.ValidationError(_('Changing the type of custom fields is not supported.')) return value @@ -545,12 +546,12 @@ class ReportInputSerializer(serializers.Serializer): def validate_schedule_at(self, value): if value and not self.context['report'].scheduling_enabled: - raise serializers.ValidationError("Scheduling is not enabled for this report.") + raise serializers.ValidationError(_("Scheduling is not enabled for this report.")) return value def validate_interval(self, value): if value and not self.context['report'].scheduling_enabled: - raise serializers.ValidationError("Scheduling is not enabled for this report.") + raise serializers.ValidationError(_("Scheduling is not enabled for this report.")) return value @@ -595,12 +596,12 @@ class ScriptInputSerializer(serializers.Serializer): def validate_schedule_at(self, value): if value and not self.context['script'].scheduling_enabled: - raise serializers.ValidationError("Scheduling is not enabled for this script.") + raise serializers.ValidationError(_("Scheduling is not enabled for this script.")) return value def validate_interval(self, value): if value and not self.context['script'].scheduling_enabled: - raise serializers.ValidationError("Scheduling is not enabled for this script.") + raise serializers.ValidationError(_("Scheduling is not enabled for this script.")) return value diff --git a/netbox/extras/conditions.py b/netbox/extras/conditions.py index db054149e..39005b752 100644 --- a/netbox/extras/conditions.py +++ b/netbox/extras/conditions.py @@ -1,5 +1,6 @@ import functools import re +from django.utils.translation import gettext as _ __all__ = ( 'Condition', @@ -50,11 +51,13 @@ class Condition: def __init__(self, attr, value, op=EQ, negate=False): if op not in self.OPERATORS: - raise ValueError(f"Unknown operator: {op}. Must be one of: {', '.join(self.OPERATORS)}") + raise ValueError(_("Unknown operator: {op}. Must be one of: {operators}").format( + op=op, operators=', '.join(self.OPERATORS) + )) if type(value) not in self.TYPES: - raise ValueError(f"Unsupported value type: {type(value)}") + raise ValueError(_("Unsupported value type: {value}").format(value=type(value))) if op not in self.TYPES[type(value)]: - raise ValueError(f"Invalid type for {op} operation: {type(value)}") + raise ValueError(_("Invalid type for {op} operation: {value}").format(op=op, value=type(value))) self.attr = attr self.value = value @@ -131,14 +134,17 @@ class ConditionSet: """ def __init__(self, ruleset): if type(ruleset) is not dict: - raise ValueError(f"Ruleset must be a dictionary, not {type(ruleset)}.") + raise ValueError(_("Ruleset must be a dictionary, not {ruleset}.").format(ruleset=type(ruleset))) if len(ruleset) != 1: - raise ValueError(f"Ruleset must have exactly one logical operator (found {len(ruleset)})") + raise ValueError(_("Ruleset must have exactly one logical operator (found {ruleset})").format( + ruleset=len(ruleset))) # Determine the logic type logic = list(ruleset.keys())[0] if type(logic) is not str or logic.lower() not in (AND, OR): - raise ValueError(f"Invalid logic type: {logic} (must be '{AND}' or '{OR}')") + raise ValueError(_("Invalid logic type: {logic} (must be '{op_and}' or '{op_or}')").format( + logic=logic, op_and=AND, op_or=OR + )) self.logic = logic.lower() # Compile the set of Conditions diff --git a/netbox/extras/dashboard/utils.py b/netbox/extras/dashboard/utils.py index 812b8db1b..20c508b2a 100644 --- a/netbox/extras/dashboard/utils.py +++ b/netbox/extras/dashboard/utils.py @@ -2,6 +2,7 @@ import uuid from django.conf import settings from django.core.exceptions import ObjectDoesNotExist +from django.utils.translation import gettext as _ from netbox.registry import registry from extras.constants import DEFAULT_DASHBOARD @@ -32,7 +33,7 @@ def get_widget_class(name): try: return registry['widgets'][name] except KeyError: - raise ValueError(f"Unregistered widget class: {name}") + raise ValueError(_("Unregistered widget class: {name}").format(name=name)) def get_dashboard(user): diff --git a/netbox/extras/dashboard/widgets.py b/netbox/extras/dashboard/widgets.py index 8cfbb4c61..d4b777fb5 100644 --- a/netbox/extras/dashboard/widgets.py +++ b/netbox/extras/dashboard/widgets.py @@ -112,7 +112,9 @@ class DashboardWidget: Params: request: The current request """ - raise NotImplementedError(f"{self.__class__} must define a render() method.") + raise NotImplementedError(_("{class_name} must define a render() method.").format( + class_name=self.__class__ + )) @property def name(self): @@ -178,7 +180,7 @@ class ObjectCountsWidget(DashboardWidget): try: dict(data) except TypeError: - raise forms.ValidationError("Invalid format. Object filters must be passed as a dictionary.") + raise forms.ValidationError(_("Invalid format. Object filters must be passed as a dictionary.")) return data def render(self, request): @@ -232,7 +234,7 @@ class ObjectListWidget(DashboardWidget): try: urlencode(data) except (TypeError, ValueError): - raise forms.ValidationError("Invalid format. URL parameters must be passed as a dictionary.") + raise forms.ValidationError(_("Invalid format. URL parameters must be passed as a dictionary.")) return data def render(self, request): diff --git a/netbox/extras/events.py b/netbox/extras/events.py index c50f4488d..e74524ceb 100644 --- a/netbox/extras/events.py +++ b/netbox/extras/events.py @@ -6,6 +6,7 @@ from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist from django.utils import timezone from django.utils.module_loading import import_string +from django.utils.translation import gettext as _ from django_rq import get_queue from core.models import Job @@ -129,7 +130,9 @@ def process_event_rules(event_rules, model_name, event, data, username=None, sna ) else: - raise ValueError(f"Unknown action type for an event rule: {event_rule.action_type}") + raise ValueError(_("Unknown action type for an event rule: {action_type}").format( + action_type=event_rule.action_type + )) def process_event_queue(events): @@ -175,4 +178,4 @@ def flush_events(queue): func = import_string(name) func(queue) except Exception as e: - logger.error(f"Cannot import events pipeline {name} error: {e}") + logger.error(_("Cannot import events pipeline {name} error: {error}").format(name=name, error=e)) diff --git a/netbox/extras/forms/bulk_import.py b/netbox/extras/forms/bulk_import.py index f8d3ffb7f..59ccc2bf9 100644 --- a/netbox/extras/forms/bulk_import.py +++ b/netbox/extras/forms/bulk_import.py @@ -202,7 +202,7 @@ class EventRuleImportForm(NetBoxModelImportForm): try: webhook = Webhook.objects.get(name=action_object) except Webhook.DoesNotExist: - raise forms.ValidationError(f"Webhook {action_object} not found") + raise forms.ValidationError(_("Webhook {name} not found").format(name=action_object)) self.instance.action_object = webhook # Script elif action_type == EventRuleActionChoices.SCRIPT: @@ -211,7 +211,7 @@ class EventRuleImportForm(NetBoxModelImportForm): try: module, script = get_module_and_script(module_name, script_name) except ObjectDoesNotExist: - raise forms.ValidationError(f"Script {action_object} not found") + raise forms.ValidationError(_("Script {name} not found").format(name=action_object)) self.instance.action_object = module self.instance.action_object_type = ContentType.objects.get_for_model(module, for_concrete_model=False) self.instance.action_parameters = { diff --git a/netbox/extras/management/commands/reindex.py b/netbox/extras/management/commands/reindex.py index e072c220a..e20fad0ce 100644 --- a/netbox/extras/management/commands/reindex.py +++ b/netbox/extras/management/commands/reindex.py @@ -1,5 +1,6 @@ from django.contrib.contenttypes.models import ContentType from django.core.management.base import BaseCommand, CommandError +from django.utils.translation import gettext as _ from netbox.registry import registry from netbox.search.backends import search_backend @@ -62,7 +63,7 @@ class Command(BaseCommand): # Determine which models to reindex indexers = self._get_indexers(*model_labels) if not indexers: - raise CommandError("No indexers found!") + raise CommandError(_("No indexers found!")) self.stdout.write(f'Reindexing {len(indexers)} models.') # Clear all cached values for the specified models (if not being lazy) diff --git a/netbox/extras/scripts.py b/netbox/extras/scripts.py index f28465547..7d86472c9 100644 --- a/netbox/extras/scripts.py +++ b/netbox/extras/scripts.py @@ -11,6 +11,7 @@ from django.conf import settings from django.core.validators import RegexValidator from django.db import transaction from django.utils.functional import classproperty +from django.utils.translation import gettext as _ from core.choices import JobStatusChoices from core.models import Job @@ -356,7 +357,7 @@ class BaseScript: return ordered_vars def run(self, data, commit): - raise NotImplementedError("The script must define a run() method.") + raise NotImplementedError(_("The script must define a run() method.")) # Form rendering @@ -367,11 +368,11 @@ class BaseScript: fieldsets.extend(self.fieldsets) else: fields = list(name for name, _ in self._get_vars().items()) - fieldsets.append(('Script Data', fields)) + fieldsets.append((_('Script Data'), fields)) # Append the default fieldset if defined in the Meta class exec_parameters = ('_schedule_at', '_interval', '_commit') if self.scheduling_enabled else ('_commit',) - fieldsets.append(('Script Execution Parameters', exec_parameters)) + fieldsets.append((_('Script Execution Parameters'), exec_parameters)) return fieldsets diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index 4439e82b4..62e2b9eca 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -3,6 +3,7 @@ from copy import deepcopy from django.core.exceptions import ObjectDoesNotExist, PermissionDenied from django.db import transaction from django.shortcuts import get_object_or_404 +from django.utils.translation import gettext as _ from django_pglocks import advisory_lock from drf_spectacular.utils import extend_schema from netaddr import IPSet @@ -379,7 +380,7 @@ class AvailablePrefixesView(AvailableObjectsView): 'vrf': parent.vrf.pk if parent.vrf else None, }) else: - raise ValidationError("Insufficient space is available to accommodate the requested prefix size(s)") + raise ValidationError(_("Insufficient space is available to accommodate the requested prefix size(s)")) return requested_objects diff --git a/netbox/ipam/fields.py b/netbox/ipam/fields.py index 2d55deae4..20341005d 100644 --- a/netbox/ipam/fields.py +++ b/netbox/ipam/fields.py @@ -1,6 +1,7 @@ from django.core.exceptions import ValidationError from django.core.validators import MinValueValidator, MaxValueValidator from django.db import models +from django.utils.translation import gettext as _ from netaddr import AddrFormatError, IPNetwork from . import lookups, validators @@ -32,7 +33,7 @@ class BaseIPField(models.Field): # Always return a netaddr.IPNetwork object. (netaddr.IPAddress does not provide a mask.) return IPNetwork(value) except AddrFormatError: - raise ValidationError("Invalid IP address format: {}".format(value)) + raise ValidationError(_("Invalid IP address format: {address}").format(address=value)) except (TypeError, ValueError) as e: raise ValidationError(e) diff --git a/netbox/ipam/formfields.py b/netbox/ipam/formfields.py index e8d171d7f..75040e89c 100644 --- a/netbox/ipam/formfields.py +++ b/netbox/ipam/formfields.py @@ -1,6 +1,7 @@ from django import forms from django.core.exceptions import ValidationError from django.core.validators import validate_ipv4_address, validate_ipv6_address +from django.utils.translation import gettext_lazy as _ from netaddr import IPAddress, IPNetwork, AddrFormatError @@ -10,7 +11,7 @@ from netaddr import IPAddress, IPNetwork, AddrFormatError class IPAddressFormField(forms.Field): default_error_messages = { - 'invalid': "Enter a valid IPv4 or IPv6 address (without a mask).", + 'invalid': _("Enter a valid IPv4 or IPv6 address (without a mask)."), } def to_python(self, value): @@ -28,19 +29,19 @@ class IPAddressFormField(forms.Field): try: validate_ipv6_address(value) except ValidationError: - raise ValidationError("Invalid IPv4/IPv6 address format: {}".format(value)) + raise ValidationError(_("Invalid IPv4/IPv6 address format: {address}").format(address=value)) try: return IPAddress(value) except ValueError: - raise ValidationError('This field requires an IP address without a mask.') + raise ValidationError(_('This field requires an IP address without a mask.')) except AddrFormatError: - raise ValidationError("Please specify a valid IPv4 or IPv6 address.") + raise ValidationError(_("Please specify a valid IPv4 or IPv6 address.")) class IPNetworkFormField(forms.Field): default_error_messages = { - 'invalid': "Enter a valid IPv4 or IPv6 address (with CIDR mask).", + 'invalid': _("Enter a valid IPv4 or IPv6 address (with CIDR mask)."), } def to_python(self, value): @@ -52,9 +53,9 @@ class IPNetworkFormField(forms.Field): # Ensure that a subnet mask has been specified. This prevents IPs from defaulting to a /32 or /128. if len(value.split('/')) != 2: - raise ValidationError('CIDR mask (e.g. /24) is required.') + raise ValidationError(_('CIDR mask (e.g. /24) is required.')) try: return IPNetwork(value) except AddrFormatError: - raise ValidationError("Please specify a valid IPv4 or IPv6 address.") + raise ValidationError(_("Please specify a valid IPv4 or IPv6 address.")) diff --git a/netbox/ipam/forms/model_forms.py b/netbox/ipam/forms/model_forms.py index 34b7c5958..c7e3f92a3 100644 --- a/netbox/ipam/forms/model_forms.py +++ b/netbox/ipam/forms/model_forms.py @@ -751,4 +751,4 @@ class ServiceCreateForm(ServiceForm): if not self.cleaned_data['description']: self.cleaned_data['description'] = service_template.description elif not all(self.cleaned_data[f] for f in ('name', 'protocol', 'ports')): - raise forms.ValidationError("Must specify name, protocol, and port(s) if not using a service template.") + raise forms.ValidationError(_("Must specify name, protocol, and port(s) if not using a service template.")) diff --git a/netbox/ipam/validators.py b/netbox/ipam/validators.py index 50faea8b8..3f1d5fdc3 100644 --- a/netbox/ipam/validators.py +++ b/netbox/ipam/validators.py @@ -1,14 +1,19 @@ from django.core.exceptions import ValidationError from django.core.validators import BaseValidator, RegexValidator +from django.utils.translation import gettext_lazy as _ def prefix_validator(prefix): if prefix.ip != prefix.cidr.ip: - raise ValidationError("{} is not a valid prefix. Did you mean {}?".format(prefix, prefix.cidr)) + raise ValidationError( + _("{prefix} is not a valid prefix. Did you mean {suggested}?").format( + prefix=prefix, suggested=prefix.cidr + ) + ) class MaxPrefixLengthValidator(BaseValidator): - message = 'The prefix length must be less than or equal to %(limit_value)s.' + message = _('The prefix length must be less than or equal to %(limit_value)s.') code = 'max_prefix_length' def compare(self, a, b): @@ -16,7 +21,7 @@ class MaxPrefixLengthValidator(BaseValidator): class MinPrefixLengthValidator(BaseValidator): - message = 'The prefix length must be greater than or equal to %(limit_value)s.' + message = _('The prefix length must be greater than or equal to %(limit_value)s.') code = 'min_prefix_length' def compare(self, a, b): @@ -25,6 +30,6 @@ class MinPrefixLengthValidator(BaseValidator): DNSValidator = RegexValidator( regex=r'^([0-9A-Za-z_-]+|\*)(\.[0-9A-Za-z_-]+)*\.?$', - message='Only alphanumeric characters, asterisks, hyphens, periods, and underscores are allowed in DNS names', + message=_('Only alphanumeric characters, asterisks, hyphens, periods, and underscores are allowed in DNS names'), code='invalid' ) diff --git a/netbox/netbox/api/fields.py b/netbox/netbox/api/fields.py index d6e43ea75..61c43c211 100644 --- a/netbox/netbox/api/fields.py +++ b/netbox/netbox/api/fields.py @@ -1,4 +1,5 @@ from django.core.exceptions import ObjectDoesNotExist +from django.utils.translation import gettext as _ from drf_spectacular.utils import extend_schema_field from drf_spectacular.types import OpenApiTypes from netaddr import IPNetwork @@ -58,11 +59,11 @@ class ChoiceField(serializers.Field): if data == '': if self.allow_blank: return data - raise ValidationError("This field may not be blank.") + raise ValidationError(_("This field may not be blank.")) # Provide an explicit error message if the request is trying to write a dict or list if isinstance(data, (dict, list)): - raise ValidationError('Value must be passed directly (e.g. "foo": 123); do not use a dictionary or list.') + raise ValidationError(_('Value must be passed directly (e.g. "foo": 123); do not use a dictionary or list.')) # Check for string representations of boolean/integer values if hasattr(data, 'lower'): @@ -82,7 +83,7 @@ class ChoiceField(serializers.Field): except TypeError: # Input is an unhashable type pass - raise ValidationError(f"{data} is not a valid choice.") + raise ValidationError(_("{value} is not a valid choice.").format(value=data)) @property def choices(self): @@ -95,8 +96,8 @@ class ContentTypeField(RelatedField): Represent a ContentType as '.' """ default_error_messages = { - "does_not_exist": "Invalid content type: {content_type}", - "invalid": "Invalid value. Specify a content type as '.'.", + "does_not_exist": _("Invalid content type: {content_type}"), + "invalid": _("Invalid value. Specify a content type as '.'."), } def to_internal_value(self, data): diff --git a/netbox/netbox/api/serializers/nested.py b/netbox/netbox/api/serializers/nested.py index 95dcd560c..027f3d11e 100644 --- a/netbox/netbox/api/serializers/nested.py +++ b/netbox/netbox/api/serializers/nested.py @@ -1,4 +1,5 @@ from django.core.exceptions import FieldError, MultipleObjectsReturned, ObjectDoesNotExist +from django.utils.translation import gettext_lazy as _ from rest_framework import serializers from rest_framework.exceptions import ValidationError @@ -30,9 +31,12 @@ class WritableNestedSerializer(BaseModelSerializer): try: return queryset.get(**params) except ObjectDoesNotExist: - raise ValidationError(f"Related object not found using the provided attributes: {params}") + raise ValidationError( + _("Related object not found using the provided attributes: {params}").format(params=params)) except MultipleObjectsReturned: - raise ValidationError(f"Multiple objects match the provided attributes: {params}") + raise ValidationError( + _("Multiple objects match the provided attributes: {params}").format(params=params) + ) except FieldError as e: raise ValidationError(e) @@ -42,15 +46,17 @@ class WritableNestedSerializer(BaseModelSerializer): pk = int(data) except (TypeError, ValueError): raise ValidationError( - f"Related objects must be referenced by numeric ID or by dictionary of attributes. Received an " - f"unrecognized value: {data}" + _( + "Related objects must be referenced by numeric ID or by dictionary of attributes. Received an " + "unrecognized value: {value}" + ).format(value=data) ) # Look up object by PK try: return self.Meta.model.objects.get(pk=pk) except ObjectDoesNotExist: - raise ValidationError(f"Related object not found using the provided numeric ID: {pk}") + raise ValidationError(_("Related object not found using the provided numeric ID: {id}").format(id=pk)) # Declared here for use by PrimaryModelSerializer, but should be imported from extras.api.nested_serializers diff --git a/netbox/netbox/authentication.py b/netbox/netbox/authentication.py index 1b0d50ca5..800df7630 100644 --- a/netbox/netbox/authentication.py +++ b/netbox/netbox/authentication.py @@ -7,6 +7,7 @@ from django.contrib.auth.backends import ModelBackend, RemoteUserBackend as _Rem from django.contrib.auth.models import Group, AnonymousUser from django.core.exceptions import ImproperlyConfigured from django.db.models import Q +from django.utils.translation import gettext_lazy as _ from users.constants import CONSTRAINT_TOKEN_USER from users.models import ObjectPermission @@ -132,7 +133,9 @@ class ObjectPermissionMixin: # Sanity check: Ensure that the requested permission applies to the specified object model = obj._meta.concrete_model if model._meta.label_lower != '.'.join((app_label, model_name)): - raise ValueError(f"Invalid permission {perm} for model {model}") + raise ValueError(_("Invalid permission {permission} for model {model}").format( + permission=perm, model=model + )) # Compile a QuerySet filter that matches all instances of the specified model tokens = { diff --git a/netbox/netbox/config/__init__.py b/netbox/netbox/config/__init__.py index c536ceadb..1c16d6769 100644 --- a/netbox/netbox/config/__init__.py +++ b/netbox/netbox/config/__init__.py @@ -4,6 +4,7 @@ import threading from django.conf import settings from django.core.cache import cache from django.db.utils import DatabaseError +from django.utils.translation import gettext_lazy as _ from .parameters import PARAMS @@ -63,7 +64,7 @@ class Config: if item in self.defaults: return self.defaults[item] - raise AttributeError(f"Invalid configuration parameter: {item}") + raise AttributeError(_("Invalid configuration parameter: {item}").format(item=item)) def _populate_from_cache(self): """Populate config data from Redis cache""" diff --git a/netbox/netbox/forms/mixins.py b/netbox/netbox/forms/mixins.py index d76eb56c8..815f1f6fa 100644 --- a/netbox/netbox/forms/mixins.py +++ b/netbox/netbox/forms/mixins.py @@ -35,7 +35,9 @@ class CustomFieldsMixin: Return the ContentType of the form's model. """ if not getattr(self, 'model', None): - raise NotImplementedError(f"{self.__class__.__name__} must specify a model class.") + raise NotImplementedError(_("{class_name} must specify a model class.").format( + class_name=self.__class__.__name__ + )) return ContentType.objects.get_for_model(self.model) def _get_custom_fields(self, content_type): diff --git a/netbox/netbox/models/features.py b/netbox/netbox/models/features.py index 6eb2b36e1..da7b38f56 100644 --- a/netbox/netbox/models/features.py +++ b/netbox/netbox/models/features.py @@ -275,16 +275,20 @@ class CustomFieldsMixin(models.Model): # Validate all field values for field_name, value in self.custom_field_data.items(): if field_name not in custom_fields: - raise ValidationError(f"Unknown field name '{field_name}' in custom field data.") + raise ValidationError(_("Unknown field name '{name}' in custom field data.").format( + name=field_name + )) try: custom_fields[field_name].validate(value) except ValidationError as e: - raise ValidationError(f"Invalid value for custom field '{field_name}': {e.message}") + raise ValidationError(_("Invalid value for custom field '{name}': {error}").format( + name=field_name, error=e.message + )) # Check for missing required values for cf in custom_fields.values(): if cf.required and cf.name not in self.custom_field_data: - raise ValidationError(f"Missing required custom field '{cf.name}'.") + raise ValidationError(_("Missing required custom field '{name}'.").format(name=cf.name)) class CustomLinksMixin(models.Model): @@ -547,7 +551,9 @@ class SyncedDataMixin(models.Model): Inheriting models must override this method with specific logic to copy data from the assigned DataFile to the local instance. This method should *NOT* call save() on the instance. """ - raise NotImplementedError(f"{self.__class__} must implement a sync_data() method.") + raise NotImplementedError(_("{class_name} must implement a sync_data() method.").format( + class_name=self.__class__ + )) # diff --git a/netbox/netbox/plugins/navigation.py b/netbox/netbox/plugins/navigation.py index 2075c97b6..aae569412 100644 --- a/netbox/netbox/plugins/navigation.py +++ b/netbox/netbox/plugins/navigation.py @@ -1,6 +1,7 @@ from netbox.navigation import MenuGroup from utilities.choices import ButtonColorChoices from django.utils.text import slugify +from django.utils.translation import gettext as _ __all__ = ( 'PluginMenu', @@ -42,11 +43,11 @@ class PluginMenuItem: self.staff_only = staff_only if permissions is not None: if type(permissions) not in (list, tuple): - raise TypeError("Permissions must be passed as a tuple or list.") + raise TypeError(_("Permissions must be passed as a tuple or list.")) self.permissions = permissions if buttons is not None: if type(buttons) not in (list, tuple): - raise TypeError("Buttons must be passed as a tuple or list.") + raise TypeError(_("Buttons must be passed as a tuple or list.")) self.buttons = buttons @@ -64,9 +65,9 @@ class PluginMenuButton: self.icon_class = icon_class if permissions is not None: if type(permissions) not in (list, tuple): - raise TypeError("Permissions must be passed as a tuple or list.") + raise TypeError(_("Permissions must be passed as a tuple or list.")) self.permissions = permissions if color is not None: if color not in ButtonColorChoices.values(): - raise ValueError("Button color must be a choice within ButtonColorChoices.") + raise ValueError(_("Button color must be a choice within ButtonColorChoices.")) self.color = color diff --git a/netbox/netbox/plugins/registration.py b/netbox/netbox/plugins/registration.py index 3be538441..fd247a82a 100644 --- a/netbox/netbox/plugins/registration.py +++ b/netbox/netbox/plugins/registration.py @@ -1,5 +1,6 @@ import inspect +from django.utils.translation import gettext_lazy as _ from netbox.registry import registry from .navigation import PluginMenu, PluginMenuButton, PluginMenuItem from .templates import PluginTemplateExtension @@ -20,18 +21,32 @@ def register_template_extensions(class_list): # Validation for template_extension in class_list: if not inspect.isclass(template_extension): - raise TypeError(f"PluginTemplateExtension class {template_extension} was passed as an instance!") + raise TypeError( + _("PluginTemplateExtension class {template_extension} was passed as an instance!").format( + template_extension=template_extension + ) + ) if not issubclass(template_extension, PluginTemplateExtension): - raise TypeError(f"{template_extension} is not a subclass of netbox.plugins.PluginTemplateExtension!") + raise TypeError( + _("{template_extension} is not a subclass of netbox.plugins.PluginTemplateExtension!").format( + template_extension=template_extension + ) + ) if template_extension.model is None: - raise TypeError(f"PluginTemplateExtension class {template_extension} does not define a valid model!") + raise TypeError( + _("PluginTemplateExtension class {template_extension} does not define a valid model!").format( + template_extension=template_extension + ) + ) registry['plugins']['template_extensions'][template_extension.model].append(template_extension) def register_menu(menu): if not isinstance(menu, PluginMenu): - raise TypeError(f"{menu} must be an instance of netbox.plugins.PluginMenu") + raise TypeError(_("{item} must be an instance of netbox.plugins.PluginMenuItem").format( + item=menu_link + )) registry['plugins']['menus'].append(menu) @@ -42,10 +57,14 @@ def register_menu_items(section_name, class_list): # Validation for menu_link in class_list: if not isinstance(menu_link, PluginMenuItem): - raise TypeError(f"{menu_link} must be an instance of netbox.plugins.PluginMenuItem") + raise TypeError(_("{menu_link} must be an instance of netbox.plugins.PluginMenuItem").format( + menu_link=menu_link + )) for button in menu_link.buttons: if not isinstance(button, PluginMenuButton): - raise TypeError(f"{button} must be an instance of netbox.plugins.PluginMenuButton") + raise TypeError(_("{button} must be an instance of netbox.plugins.PluginMenuButton").format( + button=button + )) registry['plugins']['menu_items'][section_name] = class_list diff --git a/netbox/netbox/plugins/templates.py b/netbox/netbox/plugins/templates.py index e9b9a9dca..85229dbaf 100644 --- a/netbox/netbox/plugins/templates.py +++ b/netbox/netbox/plugins/templates.py @@ -1,4 +1,5 @@ from django.template.loader import get_template +from django.utils.translation import gettext as _ __all__ = ( 'PluginTemplateExtension', @@ -31,7 +32,7 @@ class PluginTemplateExtension: if extra_context is None: extra_context = {} elif not isinstance(extra_context, dict): - raise TypeError("extra_context must be a dictionary") + raise TypeError(_("extra_context must be a dictionary")) return get_template(template_name).render({**self.context, **extra_context}) diff --git a/netbox/netbox/registry.py b/netbox/netbox/registry.py index ad8c18dcf..d783647ec 100644 --- a/netbox/netbox/registry.py +++ b/netbox/netbox/registry.py @@ -1,4 +1,5 @@ import collections +from django.utils.translation import gettext as _ class Registry(dict): @@ -10,13 +11,13 @@ class Registry(dict): try: return super().__getitem__(key) except KeyError: - raise KeyError(f"Invalid store: {key}") + raise KeyError(_("Invalid store: {key}").format(key=key)) def __setitem__(self, key, value): - raise TypeError("Cannot add stores to registry after initialization") + raise TypeError(_("Cannot add stores to registry after initialization")) def __delitem__(self, key): - raise TypeError("Cannot delete stores from registry") + raise TypeError(_("Cannot delete stores from registry")) # Initialize the global registry diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index 615db6181..d5de2bfd3 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -14,6 +14,7 @@ from django.http import HttpResponse from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.utils.safestring import mark_safe +from django.utils.translation import gettext as _ from django_tables2.export import TableExport from extras.models import ExportTemplate @@ -390,7 +391,7 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView): try: instance = prefetched_objects[object_id] except KeyError: - form.add_error('data', f"Row {i}: Object with ID {object_id} does not exist") + form.add_error('data', _("Row {i}: Object with ID {id} does not exist").format(i=i, id=object_id)) raise ValidationError('') # Take a snapshot for change logging diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index 90b6e9495..6277e1c5b 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -11,6 +11,7 @@ from django.shortcuts import redirect, render from django.urls import reverse from django.utils.html import escape from django.utils.safestring import mark_safe +from django.utils.translation import gettext as _ from extras.signals import clear_events from utilities.error_handlers import handle_protectederror @@ -101,7 +102,9 @@ class ObjectChildrenView(ObjectView, ActionsMixin, TableMixin): request: The current request parent: The parent object """ - raise NotImplementedError(f'{self.__class__.__name__} must implement get_children()') + raise NotImplementedError(_('{class_name} must implement get_children()').format( + class_name=self.__class__.__name__ + )) def prep_table_data(self, request, queryset, parent): """ diff --git a/netbox/utilities/fields.py b/netbox/utilities/fields.py index 65e01db81..7747e0101 100644 --- a/netbox/utilities/fields.py +++ b/netbox/utilities/fields.py @@ -93,7 +93,7 @@ class RestrictedGenericForeignKey(GenericForeignKey): if type(queryset) is dict: restrict_params = queryset elif queryset is not None: - raise ValueError("Custom queryset can't be used for this lookup.") + raise ValueError(_("Custom queryset can't be used for this lookup.")) # For efficiency, group the instances by content type and then do one # query per model diff --git a/netbox/utilities/forms/bulk_import.py b/netbox/utilities/forms/bulk_import.py index 57362d3dd..223766a65 100644 --- a/netbox/utilities/forms/bulk_import.py +++ b/netbox/utilities/forms/bulk_import.py @@ -49,7 +49,7 @@ class BulkImportForm(BootstrapMixin, SyncedDataMixin, forms.Form): # Determine whether we're reading from form data or an uploaded file if self.cleaned_data['data'] and import_method != ImportMethodChoices.DIRECT: - raise forms.ValidationError("Form data must be empty when uploading/selecting a file.") + raise forms.ValidationError(_("Form data must be empty when uploading/selecting a file.")) if import_method == ImportMethodChoices.UPLOAD: self.upload_file = 'upload_file' file = self.files.get('upload_file') @@ -78,7 +78,7 @@ class BulkImportForm(BootstrapMixin, SyncedDataMixin, forms.Form): elif format == ImportFormatChoices.YAML: self.cleaned_data['data'] = self._clean_yaml(data) else: - raise forms.ValidationError(f"Unknown data format: {format}") + raise forms.ValidationError(_("Unknown data format: {format}").format(format=format)) def _detect_format(self, data): """ diff --git a/netbox/utilities/forms/utils.py b/netbox/utilities/forms/utils.py index de8e22727..abd367be5 100644 --- a/netbox/utilities/forms/utils.py +++ b/netbox/utilities/forms/utils.py @@ -2,6 +2,7 @@ import re from django import forms from django.forms.models import fields_for_model +from django.utils.translation import gettext as _ from utilities.choices import unpack_grouped_choices from utilities.querysets import RestrictedQuerySet @@ -38,7 +39,7 @@ def parse_numeric_range(string, base=10): try: begin, end = int(begin.strip(), base=base), int(end.strip(), base=base) + 1 except ValueError: - raise forms.ValidationError(f'Range "{dash_range}" is invalid.') + raise forms.ValidationError(_('Range "{value}" is invalid.').format(value=dash_range)) values.extend(range(begin, end)) return sorted(set(values)) @@ -61,7 +62,7 @@ def parse_alphanumeric_range(string): begin, end = dash_range, dash_range if begin.isdigit() and end.isdigit(): if int(begin) >= int(end): - raise forms.ValidationError(f'Range "{dash_range}" is invalid.') + raise forms.ValidationError(_('Range "{value}" is invalid.').format(value=dash_range)) for n in list(range(int(begin), int(end) + 1)): values.append(n) @@ -73,10 +74,10 @@ def parse_alphanumeric_range(string): else: # Not a valid range (more than a single character) if not len(begin) == len(end) == 1: - raise forms.ValidationError(f'Range "{dash_range}" is invalid.') + raise forms.ValidationError(_('Range "{value}" is invalid.').format(value=dash_range)) if ord(begin) >= ord(end): - raise forms.ValidationError(f'Range "{dash_range}" is invalid.') + raise forms.ValidationError(_('Range "{value}" is invalid.').format(value=dash_range)) for n in list(range(ord(begin), ord(end) + 1)): values.append(chr(n)) @@ -221,18 +222,24 @@ def parse_csv(reader): if '.' in header: field, to_field = header.split('.', 1) if field in headers: - raise forms.ValidationError(f'Duplicate or conflicting column header for "{field}"') + raise forms.ValidationError(_('Duplicate or conflicting column header for "{field}"').format( + field=field + )) headers[field] = to_field else: if header in headers: - raise forms.ValidationError(f'Duplicate or conflicting column header for "{header}"') + raise forms.ValidationError(_('Duplicate or conflicting column header for "{header}"').format( + header=header + )) headers[header] = None # Parse CSV rows into a list of dictionaries mapped from the column headers. for i, row in enumerate(reader, start=1): if len(row) != len(headers): raise forms.ValidationError( - f"Row {i}: Expected {len(headers)} columns but found {len(row)}" + _("Row {i}: Expected {count_expected} columns but found {count_found}").format( + count_expected=len(headers), count_found=len(row) + ) ) row = [col.strip() for col in row] record = dict(zip(headers.keys(), row)) @@ -253,14 +260,18 @@ def validate_csv(headers, fields, required_fields): is_update = True continue if field not in fields: - raise forms.ValidationError(f'Unexpected column header "{field}" found.') + raise forms.ValidationError(_('Unexpected column header "{field}" found.').format(field=field)) if to_field and not hasattr(fields[field], 'to_field_name'): - raise forms.ValidationError(f'Column "{field}" is not a related object; cannot use dots') + raise forms.ValidationError(_('Column "{field}" is not a related object; cannot use dots').format( + field=field + )) if to_field and not hasattr(fields[field].queryset.model, to_field): - raise forms.ValidationError(f'Invalid related object attribute for column "{field}": {to_field}') + raise forms.ValidationError(_('Invalid related object attribute for column "{field}": {to_field}').format( + field=field, to_field=to_field + )) # Validate required fields (if not an update) if not is_update: for f in required_fields: if f not in headers: - raise forms.ValidationError(f'Required column header "{f}" not found.') + raise forms.ValidationError(_('Required column header "{header}" not found.').format(header=f)) diff --git a/netbox/utilities/forms/widgets/apiselect.py b/netbox/utilities/forms/widgets/apiselect.py index e4b02cb1d..9d0ea1f44 100644 --- a/netbox/utilities/forms/widgets/apiselect.py +++ b/netbox/utilities/forms/widgets/apiselect.py @@ -3,6 +3,7 @@ from typing import Dict, List, Tuple from django import forms from django.conf import settings +from django.utils.translation import gettext_lazy as _ __all__ = ( 'APISelect', @@ -119,7 +120,11 @@ class APISelect(forms.Select): update = [{'fieldName': f, 'queryParam': q} for (f, q) in self.dynamic_params.items()] self._serialize_params(key, update) except IndexError as error: - raise RuntimeError(f"Missing required value for dynamic query param: '{self.dynamic_params}'") from error + raise RuntimeError( + _("Missing required value for dynamic query param: '{dynamic_params}'").format( + dynamic_params=self.dynamic_params + ) + ) from error def _add_static_params(self): """ @@ -132,7 +137,11 @@ class APISelect(forms.Select): update = [{'queryParam': k, 'queryValue': v} for (k, v) in self.static_params.items()] self._serialize_params(key, update) except IndexError as error: - raise RuntimeError(f"Missing required value for static query param: '{self.static_params}'") from error + raise RuntimeError( + _("Missing required value for static query param: '{static_params}'").format( + static_params=self.static_params + ) + ) from error def add_query_params(self, query_params): """ diff --git a/netbox/utilities/permissions.py b/netbox/utilities/permissions.py index 813a8f944..c72a72db7 100644 --- a/netbox/utilities/permissions.py +++ b/netbox/utilities/permissions.py @@ -1,6 +1,7 @@ from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.db.models import Q +from django.utils.translation import gettext_lazy as _ __all__ = ( 'get_permission_for_model', @@ -36,7 +37,7 @@ def resolve_permission(name): action, model_name = codename.rsplit('_', 1) except ValueError: raise ValueError( - f"Invalid permission name: {name}. Must be in the format ._" + _("Invalid permission name: {name}. Must be in the format ._").format(name=name) ) return app_label, action, model_name @@ -53,7 +54,7 @@ def resolve_permission_ct(name): try: content_type = ContentType.objects.get(app_label=app_label, model=model_name) except ContentType.DoesNotExist: - raise ValueError(f"Unknown app_label/model_name for {name}") + raise ValueError(_("Unknown app_label/model_name for {name}").format(name=name)) return content_type, action diff --git a/netbox/utilities/request.py b/netbox/utilities/request.py index a5ca145e9..3ae01f326 100644 --- a/netbox/utilities/request.py +++ b/netbox/utilities/request.py @@ -1,3 +1,4 @@ +from django.utils.translation import gettext_lazy as _ from netaddr import AddrFormatError, IPAddress from urllib.parse import urlparse @@ -29,7 +30,7 @@ def get_client_ip(request, additional_headers=()): return IPAddress(ip) except AddrFormatError: # We did our best - raise ValueError(f"Invalid IP address set for {header}: {ip}") + raise ValueError(_("Invalid IP address set for {header}: {ip}").format(header=header, ip=ip)) # Could not determine the client IP address from request headers return None diff --git a/netbox/utilities/tables.py b/netbox/utilities/tables.py index 654eb02be..7f179014a 100644 --- a/netbox/utilities/tables.py +++ b/netbox/utilities/tables.py @@ -1,3 +1,4 @@ +from django.utils.translation import gettext_lazy as _ from netbox.registry import registry __all__ = ( @@ -43,5 +44,7 @@ def register_table_column(column, name, *tables): for table in tables: reg = registry['tables'][table] if name in reg: - raise ValueError(f"A column named {name} is already defined for table {table.__name__}") + raise ValueError(_("A column named {name} is already defined for table {table_name}").format( + name=name, table_name=table.__name__ + )) reg[name] = column diff --git a/netbox/utilities/testing/views.py b/netbox/utilities/testing/views.py index 0a84c5d1b..daa44b905 100644 --- a/netbox/utilities/testing/views.py +++ b/netbox/utilities/testing/views.py @@ -6,6 +6,7 @@ from django.core.exceptions import ObjectDoesNotExist from django.db.models import ForeignKey from django.test import override_settings from django.urls import reverse +from django.utils.translation import gettext as _ from extras.choices import ObjectChangeActionChoices from extras.models import ObjectChange @@ -621,7 +622,7 @@ class ViewTestCases: @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_bulk_update_objects_with_permission(self): if not hasattr(self, 'csv_update_data'): - raise NotImplementedError("The test must define csv_update_data.") + raise NotImplementedError(_("The test must define csv_update_data.")) initial_count = self._get_queryset().count() array, csv_data = self._get_update_csv_data() diff --git a/netbox/utilities/utils.py b/netbox/utilities/utils.py index 05597b80c..4a6db9093 100644 --- a/netbox/utilities/utils.py +++ b/netbox/utilities/utils.py @@ -15,6 +15,7 @@ from django.utils import timezone from django.utils.datastructures import MultiValueDict from django.utils.html import escape from django.utils.timezone import localtime +from django.utils.translation import gettext as _ from jinja2.sandbox import SandboxedEnvironment from mptt.models import MPTTModel @@ -306,13 +307,17 @@ def to_meters(length, unit): """ try: if length < 0: - raise ValueError("Length must be a positive number") + raise ValueError(_("Length must be a positive number")) except TypeError: - raise TypeError(f"Invalid value '{length}' for length (must be a number)") + raise TypeError(_("Invalid value '{length}' for length (must be a number)").format(length=length)) valid_units = CableLengthUnitChoices.values() if unit not in valid_units: - raise ValueError(f"Unknown unit {unit}. Must be one of the following: {', '.join(valid_units)}") + raise ValueError( + _("Unknown unit {unit}. Must be one of the following: {valid_units}").format( + unit=unit, valid_units=', '.join(valid_units) + ) + ) if unit == CableLengthUnitChoices.UNIT_KILOMETER: return length * 1000 @@ -326,7 +331,7 @@ def to_meters(length, unit): return length * Decimal(0.3048) if unit == CableLengthUnitChoices.UNIT_INCH: return length * Decimal(0.0254) - raise ValueError(f"Unknown unit {unit}. Must be 'km', 'm', 'cm', 'mi', 'ft', or 'in'.") + raise ValueError(_("Unknown unit {unit}. Must be 'km', 'm', 'cm', 'mi', 'ft', or 'in'.").format(unit=unit)) def to_grams(weight, unit): @@ -335,13 +340,17 @@ def to_grams(weight, unit): """ try: if weight < 0: - raise ValueError("Weight must be a positive number") + raise ValueError(_("Weight must be a positive number")) except TypeError: - raise TypeError(f"Invalid value '{weight}' for weight (must be a number)") + raise TypeError(_("Invalid value '{weight}' for weight (must be a number)").format(weight=weight)) valid_units = WeightUnitChoices.values() if unit not in valid_units: - raise ValueError(f"Unknown unit {unit}. Must be one of the following: {', '.join(valid_units)}") + raise ValueError( + _("Unknown unit {unit}. Must be one of the following: {valid_units}").format( + unit=unit, valid_units=', '.join(valid_units) + ) + ) if unit == WeightUnitChoices.UNIT_KILOGRAM: return weight * 1000 @@ -351,7 +360,7 @@ def to_grams(weight, unit): return weight * Decimal(453.592) if unit == WeightUnitChoices.UNIT_OUNCE: return weight * Decimal(28.3495) - raise ValueError(f"Unknown unit {unit}. Must be 'kg', 'g', 'lb', 'oz'.") + raise ValueError(_("Unknown unit {unit}. Must be 'kg', 'g', 'lb', 'oz'.").format(unit=unit)) def render_jinja2(template_code, context): diff --git a/netbox/utilities/validators.py b/netbox/utilities/validators.py index eaea1c34b..0e896e52a 100644 --- a/netbox/utilities/validators.py +++ b/netbox/utilities/validators.py @@ -2,6 +2,7 @@ import re from django.core.exceptions import ValidationError from django.core.validators import BaseValidator, RegexValidator, URLValidator, _lazy_re_compile +from django.utils.translation import gettext_lazy as _ from netbox.config import get_config @@ -61,4 +62,4 @@ def validate_regex(value): try: re.compile(value) except re.error: - raise ValidationError(f"{value} is not a valid regular expression.") + raise ValidationError(_("{value} is not a valid regular expression.").format(value=value)) diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 589b71f50..9c89de998 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -2,6 +2,7 @@ from django.contrib.auth.mixins import AccessMixin from django.core.exceptions import ImproperlyConfigured from django.urls import reverse from django.urls.exceptions import NoReverseMatch +from django.utils.translation import gettext_lazy as _ from netbox.registry import registry from .permissions import resolve_permission @@ -34,7 +35,9 @@ class ContentTypePermissionRequiredMixin(AccessMixin): """ Return the specific permission necessary to perform the requested action on an object. """ - raise NotImplementedError(f"{self.__class__.__name__} must implement get_required_permission()") + raise NotImplementedError(_("{self.__class__.__name__} must implement get_required_permission()").format( + class_name=self.__class__.__name__ + )) def has_permission(self): user = self.request.user @@ -68,7 +71,9 @@ class ObjectPermissionRequiredMixin(AccessMixin): """ Return the specific permission necessary to perform the requested action on an object. """ - raise NotImplementedError(f"{self.__class__.__name__} must implement get_required_permission()") + raise NotImplementedError(_("{class_name} must implement get_required_permission()").format( + class_name=self.__class__.__name__ + )) def has_permission(self): user = self.request.user @@ -89,8 +94,10 @@ class ObjectPermissionRequiredMixin(AccessMixin): if not hasattr(self, 'queryset'): raise ImproperlyConfigured( - '{} has no queryset defined. ObjectPermissionRequiredMixin may only be used on views which define ' - 'a base queryset'.format(self.__class__.__name__) + _( + '{class_name} has no queryset defined. ObjectPermissionRequiredMixin may only be used on views ' + 'which define a base queryset' + ).format(class_name=self.__class__.__name__) ) if not self.has_permission(): diff --git a/netbox/wireless/utils.py b/netbox/wireless/utils.py index d98d6a853..5494179d1 100644 --- a/netbox/wireless/utils.py +++ b/netbox/wireless/utils.py @@ -1,4 +1,5 @@ from decimal import Decimal +from django.utils.translation import gettext_lazy as _ from .choices import WirelessChannelChoices @@ -12,7 +13,7 @@ def get_channel_attr(channel, attr): Return the specified attribute of a given WirelessChannelChoices value. """ if channel not in WirelessChannelChoices.values(): - raise ValueError(f"Invalid channel value: {channel}") + raise ValueError(_("Invalid channel value: {channel}").format(channel=channel)) channel_values = channel.split('-') attrs = { @@ -22,6 +23,6 @@ def get_channel_attr(channel, attr): 'width': Decimal(channel_values[3]), } if attr not in attrs: - raise ValueError(f"Invalid channel attribute: {attr}") + raise ValueError(_("Invalid channel attribute: {name}").format(name=attr)) return attrs[attr]