mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-17 13:08:16 -06:00
15049 add missing gettext to error strings
This commit is contained in:
parent
4416657681
commit
3f305d199d
@ -102,7 +102,7 @@ class GitBackend(DataBackend):
|
|||||||
try:
|
try:
|
||||||
porcelain.clone(self.url, local_path.name, **clone_args)
|
porcelain.clone(self.url, local_path.name, **clone_args)
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
raise SyncError(f"Fetching remote data failed ({type(e).__name__}): {e}")
|
raise SyncError(_("Fetching remote data failed ({name}): {e}").format(name=type(e).__name__, e=e))
|
||||||
|
|
||||||
yield local_path.name
|
yield local_path.name
|
||||||
|
|
||||||
|
@ -181,7 +181,7 @@ class Job(models.Model):
|
|||||||
"""
|
"""
|
||||||
valid_statuses = JobStatusChoices.TERMINAL_STATE_CHOICES
|
valid_statuses = JobStatusChoices.TERMINAL_STATE_CHOICES
|
||||||
if status not in valid_statuses:
|
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
|
# Mark the job as completed
|
||||||
self.status = status
|
self.status = status
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from django.contrib.postgres.fields import ArrayField
|
from django.contrib.postgres.fields import ArrayField
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
from netaddr import AddrFormatError, EUI, eui64_unix_expanded, mac_unix_expanded
|
from netaddr import AddrFormatError, EUI, eui64_unix_expanded, mac_unix_expanded
|
||||||
|
|
||||||
from .lookups import PathContains
|
from .lookups import PathContains
|
||||||
@ -41,7 +42,7 @@ class MACAddressField(models.Field):
|
|||||||
try:
|
try:
|
||||||
return EUI(value, version=48, dialect=mac_unix_expanded_uppercase)
|
return EUI(value, version=48, dialect=mac_unix_expanded_uppercase)
|
||||||
except AddrFormatError:
|
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):
|
def db_type(self, connection):
|
||||||
return 'macaddr'
|
return 'macaddr'
|
||||||
@ -67,7 +68,7 @@ class WWNField(models.Field):
|
|||||||
try:
|
try:
|
||||||
return EUI(value, version=64, dialect=eui64_unix_expanded_uppercase)
|
return EUI(value, version=64, dialect=eui64_unix_expanded_uppercase)
|
||||||
except AddrFormatError:
|
except AddrFormatError:
|
||||||
raise ValidationError(f"Invalid WWN format: {value}")
|
raise ValidationError(_("Invalid WWN format: {value}").format(value=value))
|
||||||
|
|
||||||
def db_type(self, connection):
|
def db_type(self, connection):
|
||||||
return 'macaddr8'
|
return 'macaddr8'
|
||||||
|
@ -870,7 +870,9 @@ class InterfaceImportForm(NetBoxModelImportForm):
|
|||||||
def clean_vdcs(self):
|
def clean_vdcs(self):
|
||||||
for vdc in self.cleaned_data['vdcs']:
|
for vdc in self.cleaned_data['vdcs']:
|
||||||
if vdc.device != self.cleaned_data['device']:
|
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']
|
return self.cleaned_data['vdcs']
|
||||||
|
|
||||||
|
|
||||||
@ -1075,7 +1077,8 @@ class InventoryItemImportForm(NetBoxModelImportForm):
|
|||||||
component = model.objects.get(device=device, name=component_name)
|
component = model.objects.get(device=device, name=component_name)
|
||||||
self.instance.component = component
|
self.instance.component = component
|
||||||
except ObjectDoesNotExist:
|
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,9 +1196,13 @@ class CableImportForm(NetBoxModelImportForm):
|
|||||||
else:
|
else:
|
||||||
termination_object = model.objects.get(device=device, name=name)
|
termination_object = model.objects.get(device=device, name=name)
|
||||||
if termination_object.cable is not None and termination_object.cable != self.instance:
|
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:
|
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])
|
setattr(self.instance, f'{side}_terminations', [termination_object])
|
||||||
return termination_object
|
return termination_object
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
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.utils import extend_schema_field
|
||||||
from drf_spectacular.types import OpenApiTypes
|
from drf_spectacular.types import OpenApiTypes
|
||||||
from rest_framework.fields import Field
|
from rest_framework.fields import Field
|
||||||
@ -88,7 +89,7 @@ class CustomFieldsDataField(Field):
|
|||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
data[cf.name] = [obj['id'] for obj in serializer.data] if many else serializer.data['id']
|
data[cf.name] = [obj['id'] for obj in serializer.data] if many else serializer.data['id']
|
||||||
else:
|
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 updating an existing instance, start with existing custom_field_data
|
||||||
if self.parent.instance:
|
if self.parent.instance:
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import functools
|
import functools
|
||||||
import re
|
import re
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'Condition',
|
'Condition',
|
||||||
@ -50,11 +51,12 @@ class Condition:
|
|||||||
|
|
||||||
def __init__(self, attr, value, op=EQ, negate=False):
|
def __init__(self, attr, value, op=EQ, negate=False):
|
||||||
if op not in self.OPERATORS:
|
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:
|
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)]:
|
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.attr = attr
|
||||||
self.value = value
|
self.value = value
|
||||||
@ -131,14 +133,16 @@ class ConditionSet:
|
|||||||
"""
|
"""
|
||||||
def __init__(self, ruleset):
|
def __init__(self, ruleset):
|
||||||
if type(ruleset) is not dict:
|
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:
|
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
|
# Determine the logic type
|
||||||
logic = list(ruleset.keys())[0]
|
logic = list(ruleset.keys())[0]
|
||||||
if type(logic) is not str or logic.lower() not in (AND, OR):
|
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()
|
self.logic = logic.lower()
|
||||||
|
|
||||||
# Compile the set of Conditions
|
# Compile the set of Conditions
|
||||||
|
@ -2,6 +2,7 @@ import uuid
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from netbox.registry import registry
|
from netbox.registry import registry
|
||||||
from extras.constants import DEFAULT_DASHBOARD
|
from extras.constants import DEFAULT_DASHBOARD
|
||||||
@ -32,7 +33,7 @@ def get_widget_class(name):
|
|||||||
try:
|
try:
|
||||||
return registry['widgets'][name]
|
return registry['widgets'][name]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise ValueError(f"Unregistered widget class: {name}")
|
raise ValueError(_("Unregistered widget class: {name}").format(name=name))
|
||||||
|
|
||||||
|
|
||||||
def get_dashboard(user):
|
def get_dashboard(user):
|
||||||
|
@ -112,7 +112,8 @@ class DashboardWidget:
|
|||||||
Params:
|
Params:
|
||||||
request: The current request
|
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
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
@ -6,6 +6,7 @@ from django.contrib.contenttypes.models import ContentType
|
|||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.module_loading import import_string
|
from django.utils.module_loading import import_string
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
from django_rq import get_queue
|
from django_rq import get_queue
|
||||||
|
|
||||||
from core.models import Job
|
from core.models import Job
|
||||||
@ -129,7 +130,8 @@ def process_event_rules(event_rules, model_name, event, data, username=None, sna
|
|||||||
)
|
)
|
||||||
|
|
||||||
else:
|
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):
|
def process_event_queue(events):
|
||||||
@ -175,4 +177,4 @@ def flush_events(queue):
|
|||||||
func = import_string(name)
|
func = import_string(name)
|
||||||
func(queue)
|
func(queue)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Cannot import events pipeline {name} error: {e}")
|
logger.error(_("Cannot import events pipeline {name} error: {e}").format(name=name, e=e))
|
||||||
|
@ -202,7 +202,7 @@ class EventRuleImportForm(NetBoxModelImportForm):
|
|||||||
try:
|
try:
|
||||||
webhook = Webhook.objects.get(name=action_object)
|
webhook = Webhook.objects.get(name=action_object)
|
||||||
except Webhook.DoesNotExist:
|
except Webhook.DoesNotExist:
|
||||||
raise forms.ValidationError(f"Webhook {action_object} not found")
|
raise forms.ValidationError(_("Webhook {action_object} not found").format(action_object=action_object))
|
||||||
self.instance.action_object = webhook
|
self.instance.action_object = webhook
|
||||||
# Script
|
# Script
|
||||||
elif action_type == EventRuleActionChoices.SCRIPT:
|
elif action_type == EventRuleActionChoices.SCRIPT:
|
||||||
@ -211,7 +211,7 @@ class EventRuleImportForm(NetBoxModelImportForm):
|
|||||||
try:
|
try:
|
||||||
module, script = get_module_and_script(module_name, script_name)
|
module, script = get_module_and_script(module_name, script_name)
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
raise forms.ValidationError(f"Script {action_object} not found")
|
raise forms.ValidationError(_("Script {action_object} not found").format(action_object=action_object))
|
||||||
self.instance.action_object = module
|
self.instance.action_object = module
|
||||||
self.instance.action_object_type = ContentType.objects.get_for_model(module, for_concrete_model=False)
|
self.instance.action_object_type = ContentType.objects.get_for_model(module, for_concrete_model=False)
|
||||||
self.instance.action_parameters = {
|
self.instance.action_parameters = {
|
||||||
|
@ -83,7 +83,7 @@ class ChoiceField(serializers.Field):
|
|||||||
except TypeError: # Input is an unhashable type
|
except TypeError: # Input is an unhashable type
|
||||||
pass
|
pass
|
||||||
|
|
||||||
raise ValidationError(f"{data} is not a valid choice.")
|
raise ValidationError(_("{data} is not a valid choice.").format(data=data))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def choices(self):
|
def choices(self):
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from django.core.exceptions import FieldError, MultipleObjectsReturned, ObjectDoesNotExist
|
from django.core.exceptions import FieldError, MultipleObjectsReturned, ObjectDoesNotExist
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
@ -30,9 +31,10 @@ class WritableNestedSerializer(BaseModelSerializer):
|
|||||||
try:
|
try:
|
||||||
return queryset.get(**params)
|
return queryset.get(**params)
|
||||||
except ObjectDoesNotExist:
|
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:
|
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:
|
except FieldError as e:
|
||||||
raise ValidationError(e)
|
raise ValidationError(e)
|
||||||
|
|
||||||
@ -42,15 +44,14 @@ class WritableNestedSerializer(BaseModelSerializer):
|
|||||||
pk = int(data)
|
pk = int(data)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
f"Related objects must be referenced by numeric ID or by dictionary of attributes. Received an "
|
_("Related objects must be referenced by numeric ID or by dictionary of attributes. Received an unrecognized value: {data}").format(data=data)
|
||||||
f"unrecognized value: {data}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Look up object by PK
|
# Look up object by PK
|
||||||
try:
|
try:
|
||||||
return self.Meta.model.objects.get(pk=pk)
|
return self.Meta.model.objects.get(pk=pk)
|
||||||
except ObjectDoesNotExist:
|
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: {pk}").format(pk=pk))
|
||||||
|
|
||||||
|
|
||||||
# Declared here for use by PrimaryModelSerializer, but should be imported from extras.api.nested_serializers
|
# Declared here for use by PrimaryModelSerializer, but should be imported from extras.api.nested_serializers
|
||||||
|
@ -7,6 +7,7 @@ from django.contrib.auth.backends import ModelBackend, RemoteUserBackend as _Rem
|
|||||||
from django.contrib.auth.models import Group, AnonymousUser
|
from django.contrib.auth.models import Group, AnonymousUser
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from users.constants import CONSTRAINT_TOKEN_USER
|
from users.constants import CONSTRAINT_TOKEN_USER
|
||||||
from users.models import ObjectPermission
|
from users.models import ObjectPermission
|
||||||
@ -132,7 +133,7 @@ class ObjectPermissionMixin:
|
|||||||
# Sanity check: Ensure that the requested permission applies to the specified object
|
# Sanity check: Ensure that the requested permission applies to the specified object
|
||||||
model = obj._meta.concrete_model
|
model = obj._meta.concrete_model
|
||||||
if model._meta.label_lower != '.'.join((app_label, model_name)):
|
if model._meta.label_lower != '.'.join((app_label, model_name)):
|
||||||
raise ValueError(f"Invalid permission {perm} for model {model}")
|
raise ValueError(_("Invalid permission {perm} for model {model}").format(perm=perm, model=model))
|
||||||
|
|
||||||
# Compile a QuerySet filter that matches all instances of the specified model
|
# Compile a QuerySet filter that matches all instances of the specified model
|
||||||
tokens = {
|
tokens = {
|
||||||
|
@ -4,6 +4,7 @@ import threading
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.db.utils import DatabaseError
|
from django.db.utils import DatabaseError
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from .parameters import PARAMS
|
from .parameters import PARAMS
|
||||||
|
|
||||||
@ -63,7 +64,7 @@ class Config:
|
|||||||
if item in self.defaults:
|
if item in self.defaults:
|
||||||
return self.defaults[item]
|
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):
|
def _populate_from_cache(self):
|
||||||
"""Populate config data from Redis cache"""
|
"""Populate config data from Redis cache"""
|
||||||
|
@ -35,7 +35,8 @@ class CustomFieldsMixin:
|
|||||||
Return the ContentType of the form's model.
|
Return the ContentType of the form's model.
|
||||||
"""
|
"""
|
||||||
if not getattr(self, 'model', None):
|
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)
|
return ContentType.objects.get_for_model(self.model)
|
||||||
|
|
||||||
def _get_custom_fields(self, content_type):
|
def _get_custom_fields(self, content_type):
|
||||||
|
@ -275,16 +275,18 @@ class CustomFieldsMixin(models.Model):
|
|||||||
# Validate all field values
|
# Validate all field values
|
||||||
for field_name, value in self.custom_field_data.items():
|
for field_name, value in self.custom_field_data.items():
|
||||||
if field_name not in custom_fields:
|
if field_name not in custom_fields:
|
||||||
raise ValidationError(f"Unknown field name '{field_name}' in custom field data.")
|
raise ValidationError(_("Unknown field name '{field_name}' in custom field data.").format(
|
||||||
|
field_name=field_name))
|
||||||
try:
|
try:
|
||||||
custom_fields[field_name].validate(value)
|
custom_fields[field_name].validate(value)
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
raise ValidationError(f"Invalid value for custom field '{field_name}': {e.message}")
|
raise ValidationError(_("Invalid value for custom field '{field_name}': {message}").format(
|
||||||
|
field_name=field_name, message=e.message))
|
||||||
|
|
||||||
# Check for missing required values
|
# Check for missing required values
|
||||||
for cf in custom_fields.values():
|
for cf in custom_fields.values():
|
||||||
if cf.required and cf.name not in self.custom_field_data:
|
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):
|
class CustomLinksMixin(models.Model):
|
||||||
@ -547,7 +549,8 @@ class SyncedDataMixin(models.Model):
|
|||||||
Inheriting models must override this method with specific logic to copy data from the assigned DataFile
|
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.
|
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__))
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import inspect
|
import inspect
|
||||||
|
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from netbox.registry import registry
|
from netbox.registry import registry
|
||||||
from .navigation import PluginMenu, PluginMenuButton, PluginMenuItem
|
from .navigation import PluginMenu, PluginMenuButton, PluginMenuItem
|
||||||
from .templates import PluginTemplateExtension
|
from .templates import PluginTemplateExtension
|
||||||
@ -20,18 +21,24 @@ def register_template_extensions(class_list):
|
|||||||
# Validation
|
# Validation
|
||||||
for template_extension in class_list:
|
for template_extension in class_list:
|
||||||
if not inspect.isclass(template_extension):
|
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):
|
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:
|
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)
|
registry['plugins']['template_extensions'][template_extension.model].append(template_extension)
|
||||||
|
|
||||||
|
|
||||||
def register_menu(menu):
|
def register_menu(menu):
|
||||||
if not isinstance(menu, PluginMenu):
|
if not isinstance(menu, PluginMenu):
|
||||||
raise TypeError(f"{menu} must be an instance of netbox.plugins.PluginMenu")
|
raise TypeError(_("{menu} must be an instance of netbox.plugins.PluginMenu").format(menu=menu))
|
||||||
registry['plugins']['menus'].append(menu)
|
registry['plugins']['menus'].append(menu)
|
||||||
|
|
||||||
|
|
||||||
@ -42,10 +49,12 @@ def register_menu_items(section_name, class_list):
|
|||||||
# Validation
|
# Validation
|
||||||
for menu_link in class_list:
|
for menu_link in class_list:
|
||||||
if not isinstance(menu_link, PluginMenuItem):
|
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:
|
for button in menu_link.buttons:
|
||||||
if not isinstance(button, PluginMenuButton):
|
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
|
registry['plugins']['menu_items'][section_name] = class_list
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ class Registry(dict):
|
|||||||
try:
|
try:
|
||||||
return super().__getitem__(key)
|
return super().__getitem__(key)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise KeyError(f"Invalid store: {key}")
|
raise KeyError(_("Invalid store: {key}").format(key=key))
|
||||||
|
|
||||||
def __setitem__(self, key, value):
|
def __setitem__(self, key, value):
|
||||||
raise TypeError(_("Cannot add stores to registry after initialization"))
|
raise TypeError(_("Cannot add stores to registry after initialization"))
|
||||||
|
@ -14,6 +14,7 @@ from django.http import HttpResponse
|
|||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
from django_tables2.export import TableExport
|
from django_tables2.export import TableExport
|
||||||
|
|
||||||
from extras.models import ExportTemplate
|
from extras.models import ExportTemplate
|
||||||
@ -390,7 +391,7 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
|
|||||||
try:
|
try:
|
||||||
instance = prefetched_objects[object_id]
|
instance = prefetched_objects[object_id]
|
||||||
except KeyError:
|
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 {object_id} does not exist").format(i=i, object_id=object_id))
|
||||||
raise ValidationError('')
|
raise ValidationError('')
|
||||||
|
|
||||||
# Take a snapshot for change logging
|
# Take a snapshot for change logging
|
||||||
|
@ -11,6 +11,7 @@ from django.shortcuts import redirect, render
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from extras.signals import clear_events
|
from extras.signals import clear_events
|
||||||
from utilities.error_handlers import handle_protectederror
|
from utilities.error_handlers import handle_protectederror
|
||||||
@ -101,7 +102,8 @@ class ObjectChildrenView(ObjectView, ActionsMixin, TableMixin):
|
|||||||
request: The current request
|
request: The current request
|
||||||
parent: The parent object
|
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):
|
def prep_table_data(self, request, queryset, parent):
|
||||||
"""
|
"""
|
||||||
|
@ -78,7 +78,7 @@ class BulkImportForm(BootstrapMixin, SyncedDataMixin, forms.Form):
|
|||||||
elif format == ImportFormatChoices.YAML:
|
elif format == ImportFormatChoices.YAML:
|
||||||
self.cleaned_data['data'] = self._clean_yaml(data)
|
self.cleaned_data['data'] = self._clean_yaml(data)
|
||||||
else:
|
else:
|
||||||
raise forms.ValidationError(f"Unknown data format: {format}")
|
raise forms.ValidationError(_("Unknown data format: {format}").format(format=format))
|
||||||
|
|
||||||
def _detect_format(self, data):
|
def _detect_format(self, data):
|
||||||
"""
|
"""
|
||||||
|
@ -2,6 +2,7 @@ import re
|
|||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.forms.models import fields_for_model
|
from django.forms.models import fields_for_model
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from utilities.choices import unpack_grouped_choices
|
from utilities.choices import unpack_grouped_choices
|
||||||
from utilities.querysets import RestrictedQuerySet
|
from utilities.querysets import RestrictedQuerySet
|
||||||
@ -38,7 +39,7 @@ def parse_numeric_range(string, base=10):
|
|||||||
try:
|
try:
|
||||||
begin, end = int(begin.strip(), base=base), int(end.strip(), base=base) + 1
|
begin, end = int(begin.strip(), base=base), int(end.strip(), base=base) + 1
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise forms.ValidationError(f'Range "{dash_range}" is invalid.')
|
raise forms.ValidationError(_('Range "{dash_range}" is invalid.').format(dash_range=dash_range))
|
||||||
values.extend(range(begin, end))
|
values.extend(range(begin, end))
|
||||||
return sorted(set(values))
|
return sorted(set(values))
|
||||||
|
|
||||||
@ -61,7 +62,7 @@ def parse_alphanumeric_range(string):
|
|||||||
begin, end = dash_range, dash_range
|
begin, end = dash_range, dash_range
|
||||||
if begin.isdigit() and end.isdigit():
|
if begin.isdigit() and end.isdigit():
|
||||||
if int(begin) >= int(end):
|
if int(begin) >= int(end):
|
||||||
raise forms.ValidationError(f'Range "{dash_range}" is invalid.')
|
raise forms.ValidationError(_('Range "{dash_range}" is invalid.').format(dash_range=dash_range))
|
||||||
|
|
||||||
for n in list(range(int(begin), int(end) + 1)):
|
for n in list(range(int(begin), int(end) + 1)):
|
||||||
values.append(n)
|
values.append(n)
|
||||||
@ -73,10 +74,10 @@ def parse_alphanumeric_range(string):
|
|||||||
else:
|
else:
|
||||||
# Not a valid range (more than a single character)
|
# Not a valid range (more than a single character)
|
||||||
if not len(begin) == len(end) == 1:
|
if not len(begin) == len(end) == 1:
|
||||||
raise forms.ValidationError(f'Range "{dash_range}" is invalid.')
|
raise forms.ValidationError(_('Range "{dash_range}" is invalid.').format(dash_range=dash_range))
|
||||||
|
|
||||||
if ord(begin) >= ord(end):
|
if ord(begin) >= ord(end):
|
||||||
raise forms.ValidationError(f'Range "{dash_range}" is invalid.')
|
raise forms.ValidationError(_('Range "{dash_range}" is invalid.').format(dash_range=dash_range))
|
||||||
|
|
||||||
for n in list(range(ord(begin), ord(end) + 1)):
|
for n in list(range(ord(begin), ord(end) + 1)):
|
||||||
values.append(chr(n))
|
values.append(chr(n))
|
||||||
@ -221,18 +222,21 @@ def parse_csv(reader):
|
|||||||
if '.' in header:
|
if '.' in header:
|
||||||
field, to_field = header.split('.', 1)
|
field, to_field = header.split('.', 1)
|
||||||
if field in headers:
|
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
|
headers[field] = to_field
|
||||||
else:
|
else:
|
||||||
if header in headers:
|
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
|
headers[header] = None
|
||||||
|
|
||||||
# Parse CSV rows into a list of dictionaries mapped from the column headers.
|
# Parse CSV rows into a list of dictionaries mapped from the column headers.
|
||||||
for i, row in enumerate(reader, start=1):
|
for i, row in enumerate(reader, start=1):
|
||||||
if len(row) != len(headers):
|
if len(row) != len(headers):
|
||||||
raise forms.ValidationError(
|
raise forms.ValidationError(
|
||||||
f"Row {i}: Expected {len(headers)} columns but found {len(row)}"
|
_("Row {i}: Expected {len_headers} columns but found {len_row}").format(
|
||||||
|
len_headers=len(headers), len_row=len(row))
|
||||||
)
|
)
|
||||||
row = [col.strip() for col in row]
|
row = [col.strip() for col in row]
|
||||||
record = dict(zip(headers.keys(), row))
|
record = dict(zip(headers.keys(), row))
|
||||||
@ -253,14 +257,16 @@ def validate_csv(headers, fields, required_fields):
|
|||||||
is_update = True
|
is_update = True
|
||||||
continue
|
continue
|
||||||
if field not in fields:
|
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'):
|
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):
|
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)
|
# Validate required fields (if not an update)
|
||||||
if not is_update:
|
if not is_update:
|
||||||
for f in required_fields:
|
for f in required_fields:
|
||||||
if f not in headers:
|
if f not in headers:
|
||||||
raise forms.ValidationError(f'Required column header "{f}" not found.')
|
raise forms.ValidationError(_('Required column header "{f}" not found.').format(f=f))
|
||||||
|
@ -3,6 +3,7 @@ from typing import Dict, List, Tuple
|
|||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'APISelect',
|
'APISelect',
|
||||||
@ -119,7 +120,9 @@ class APISelect(forms.Select):
|
|||||||
update = [{'fieldName': f, 'queryParam': q} for (f, q) in self.dynamic_params.items()]
|
update = [{'fieldName': f, 'queryParam': q} for (f, q) in self.dynamic_params.items()]
|
||||||
self._serialize_params(key, update)
|
self._serialize_params(key, update)
|
||||||
except IndexError as error:
|
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):
|
def _add_static_params(self):
|
||||||
"""
|
"""
|
||||||
@ -132,7 +135,9 @@ class APISelect(forms.Select):
|
|||||||
update = [{'queryParam': k, 'queryValue': v} for (k, v) in self.static_params.items()]
|
update = [{'queryParam': k, 'queryValue': v} for (k, v) in self.static_params.items()]
|
||||||
self._serialize_params(key, update)
|
self._serialize_params(key, update)
|
||||||
except IndexError as error:
|
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):
|
def add_query_params(self, query_params):
|
||||||
"""
|
"""
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'get_permission_for_model',
|
'get_permission_for_model',
|
||||||
@ -36,7 +37,7 @@ def resolve_permission(name):
|
|||||||
action, model_name = codename.rsplit('_', 1)
|
action, model_name = codename.rsplit('_', 1)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Invalid permission name: {name}. Must be in the format <app_label>.<action>_<model>"
|
_("Invalid permission name: {name}. Must be in the format <app_label>.<action>_<model>").format(name=name)
|
||||||
)
|
)
|
||||||
|
|
||||||
return app_label, action, model_name
|
return app_label, action, model_name
|
||||||
@ -53,7 +54,7 @@ def resolve_permission_ct(name):
|
|||||||
try:
|
try:
|
||||||
content_type = ContentType.objects.get(app_label=app_label, model=model_name)
|
content_type = ContentType.objects.get(app_label=app_label, model=model_name)
|
||||||
except ContentType.DoesNotExist:
|
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
|
return content_type, action
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from netaddr import AddrFormatError, IPAddress
|
from netaddr import AddrFormatError, IPAddress
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
@ -29,7 +30,7 @@ def get_client_ip(request, additional_headers=()):
|
|||||||
return IPAddress(ip)
|
return IPAddress(ip)
|
||||||
except AddrFormatError:
|
except AddrFormatError:
|
||||||
# We did our best
|
# 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
|
# Could not determine the client IP address from request headers
|
||||||
return None
|
return None
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from netbox.registry import registry
|
from netbox.registry import registry
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -43,5 +44,6 @@ def register_table_column(column, name, *tables):
|
|||||||
for table in tables:
|
for table in tables:
|
||||||
reg = registry['tables'][table]
|
reg = registry['tables'][table]
|
||||||
if name in reg:
|
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
|
reg[name] = column
|
||||||
|
@ -2,6 +2,7 @@ import re
|
|||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import BaseValidator, RegexValidator, URLValidator, _lazy_re_compile
|
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
|
from netbox.config import get_config
|
||||||
|
|
||||||
@ -61,4 +62,4 @@ def validate_regex(value):
|
|||||||
try:
|
try:
|
||||||
re.compile(value)
|
re.compile(value)
|
||||||
except re.error:
|
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))
|
||||||
|
@ -2,6 +2,7 @@ from django.contrib.auth.mixins import AccessMixin
|
|||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.urls.exceptions import NoReverseMatch
|
from django.urls.exceptions import NoReverseMatch
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from netbox.registry import registry
|
from netbox.registry import registry
|
||||||
from .permissions import resolve_permission
|
from .permissions import resolve_permission
|
||||||
@ -34,7 +35,8 @@ class ContentTypePermissionRequiredMixin(AccessMixin):
|
|||||||
"""
|
"""
|
||||||
Return the specific permission necessary to perform the requested action on an object.
|
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):
|
def has_permission(self):
|
||||||
user = self.request.user
|
user = self.request.user
|
||||||
@ -68,7 +70,8 @@ class ObjectPermissionRequiredMixin(AccessMixin):
|
|||||||
"""
|
"""
|
||||||
Return the specific permission necessary to perform the requested action on an object.
|
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):
|
def has_permission(self):
|
||||||
user = self.request.user
|
user = self.request.user
|
||||||
@ -89,8 +92,8 @@ class ObjectPermissionRequiredMixin(AccessMixin):
|
|||||||
|
|
||||||
if not hasattr(self, 'queryset'):
|
if not hasattr(self, 'queryset'):
|
||||||
raise ImproperlyConfigured(
|
raise ImproperlyConfigured(
|
||||||
'{} has no queryset defined. ObjectPermissionRequiredMixin may only be used on views which define '
|
_('{} has no queryset defined. ObjectPermissionRequiredMixin may only be used on views which define a base queryset').format(
|
||||||
'a base queryset'.format(self.__class__.__name__)
|
self.__class__.__name__)
|
||||||
)
|
)
|
||||||
|
|
||||||
if not self.has_permission():
|
if not self.has_permission():
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from .choices import WirelessChannelChoices
|
from .choices import WirelessChannelChoices
|
||||||
|
|
||||||
@ -12,7 +13,7 @@ def get_channel_attr(channel, attr):
|
|||||||
Return the specified attribute of a given WirelessChannelChoices value.
|
Return the specified attribute of a given WirelessChannelChoices value.
|
||||||
"""
|
"""
|
||||||
if channel not in WirelessChannelChoices.values():
|
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('-')
|
channel_values = channel.split('-')
|
||||||
attrs = {
|
attrs = {
|
||||||
@ -22,6 +23,6 @@ def get_channel_attr(channel, attr):
|
|||||||
'width': Decimal(channel_values[3]),
|
'width': Decimal(channel_values[3]),
|
||||||
}
|
}
|
||||||
if attr not in attrs:
|
if attr not in attrs:
|
||||||
raise ValueError(f"Invalid channel attribute: {attr}")
|
raise ValueError(_("Invalid channel attribute: {attr}").format(attr=attr))
|
||||||
|
|
||||||
return attrs[attr]
|
return attrs[attr]
|
||||||
|
Loading…
Reference in New Issue
Block a user