Merge branch 'develop' into feature

This commit is contained in:
Jeremy Stretch
2024-02-21 16:24:23 -05:00
98 changed files with 9468 additions and 4748 deletions

View File

@@ -77,7 +77,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

View File

@@ -48,7 +48,7 @@ class BulkImportForm(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')
@@ -77,7 +77,7 @@ class BulkImportForm(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):
"""

View File

@@ -93,6 +93,8 @@ class JSONField(_JSONField):
"""
Custom wrapper around Django's built-in JSONField to avoid presenting "null" as the default text.
"""
empty_values = [None, '', ()]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not self.help_text:

View File

@@ -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 {row}: Expected {count_expected} columns but found {count_found}").format(
row=i, 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))

View File

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

View File

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

View File

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

View File

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

View File

@@ -25,7 +25,7 @@
<hr class="dropdown-divider">
</li>
<li>
<a class="dropdown-item" href="{% url 'extras:exporttemplate_add' %}?content_type={{ content_type.pk }}">{% trans "Add export template" %}...</a>
<a class="dropdown-item" href="{% url 'extras:exporttemplate_add' %}?content_types={{ content_type.pk }}">{% trans "Add export template" %}...</a>
</li>
{% endif %}
</ul>

View File

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

View File

@@ -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
@@ -307,13 +308,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
@@ -327,7 +332,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):
@@ -336,13 +341,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
@@ -352,7 +361,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):

View File

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

View File

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