diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py
index 4aba73fde..c21718244 100644
--- a/netbox/dcim/models/devices.py
+++ b/netbox/dcim/models/devices.py
@@ -12,7 +12,7 @@ from django.db.models.functions import Lower
from django.db.models.signals import post_save
from django.urls import reverse
from django.utils.safestring import mark_safe
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
from dcim.choices import *
from dcim.constants import *
@@ -78,9 +78,11 @@ class DeviceType(PrimaryModel, WeightMixin):
related_name='device_types'
)
model = models.CharField(
+ verbose_name=_('model'),
max_length=100
)
slug = models.SlugField(
+ verbose_name=_('slug'),
max_length=100
)
default_platform = models.ForeignKey(
@@ -89,9 +91,10 @@ class DeviceType(PrimaryModel, WeightMixin):
related_name='+',
blank=True,
null=True,
- verbose_name='Default platform'
+ verbose_name=_('Default platform')
)
part_number = models.CharField(
+ verbose_name=_('part number'),
max_length=50,
blank=True,
help_text=_('Discrete part number (optional)')
@@ -100,22 +103,23 @@ class DeviceType(PrimaryModel, WeightMixin):
max_digits=4,
decimal_places=1,
default=1.0,
- verbose_name='Height (U)'
+ verbose_name=_('Height (U)')
)
is_full_depth = models.BooleanField(
default=True,
- verbose_name='Is full depth',
+ verbose_name=_('Is full depth'),
help_text=_('Device consumes both front and rear rack faces')
)
subdevice_role = models.CharField(
max_length=50,
choices=SubdeviceRoleChoices,
blank=True,
- verbose_name='Parent/child status',
+ verbose_name=_('Parent/child status'),
help_text=_('Parent devices house child devices in device bays. Leave blank '
'if this device type is neither a parent nor a child.')
)
airflow = models.CharField(
+ verbose_name=_('airflow'),
max_length=50,
choices=DeviceAirflowChoices,
blank=True
@@ -277,7 +281,7 @@ class DeviceType(PrimaryModel, WeightMixin):
# U height must be divisible by 0.5
if self.u_height % decimal.Decimal(0.5):
raise ValidationError({
- 'u_height': "U height must be in increments of 0.5 rack units."
+ 'u_height': _("U height must be in increments of 0.5 rack units.")
})
# If editing an existing DeviceType to have a larger u_height, first validate that *all* instances of it have
@@ -293,8 +297,8 @@ class DeviceType(PrimaryModel, WeightMixin):
)
if d.position not in u_available:
raise ValidationError({
- 'u_height': "Device {} in rack {} does not have sufficient space to accommodate a height of "
- "{}U".format(d, d.rack, self.u_height)
+ 'u_height': _("Device {} in rack {} does not have sufficient space to accommodate a height of "
+ "{}U").format(d, d.rack, self.u_height)
})
# If modifying the height of an existing DeviceType to 0U, check for any instances assigned to a rack position.
@@ -307,8 +311,8 @@ class DeviceType(PrimaryModel, WeightMixin):
url = f"{reverse('dcim:device_list')}?manufactuer_id={self.manufacturer_id}&device_type_id={self.pk}"
raise ValidationError({
'u_height': mark_safe(
- f'Unable to set 0U height: Found {racked_instance_count} instances already '
- f'mounted within racks.'
+ _('Unable to set 0U height: Found {racked_instance_count} instances already '
+ 'mounted within racks.').format(url=url, racked_instance_count=racked_instance_count)
)
})
@@ -316,13 +320,13 @@ class DeviceType(PrimaryModel, WeightMixin):
self.subdevice_role != SubdeviceRoleChoices.ROLE_PARENT
) and self.pk and self.devicebaytemplates.count():
raise ValidationError({
- 'subdevice_role': "Must delete all device bay templates associated with this device before "
- "declassifying it as a parent device."
+ 'subdevice_role': _("Must delete all device bay templates associated with this device before "
+ "declassifying it as a parent device.")
})
if self.u_height and self.subdevice_role == SubdeviceRoleChoices.ROLE_CHILD:
raise ValidationError({
- 'u_height': "Child device types must be 0U."
+ 'u_height': _("Child device types must be 0U.")
})
def save(self, *args, **kwargs):
@@ -367,9 +371,11 @@ class ModuleType(PrimaryModel, WeightMixin):
related_name='module_types'
)
model = models.CharField(
+ verbose_name=_('model'),
max_length=100
)
part_number = models.CharField(
+ verbose_name=_('part number'),
max_length=50,
blank=True,
help_text=_('Discrete part number (optional)')
@@ -454,11 +460,12 @@ class DeviceRole(OrganizationalModel):
virtual machines as well.
"""
color = ColorField(
+ verbose_name=_('color'),
default=ColorChoices.COLOR_GREY
)
vm_role = models.BooleanField(
default=True,
- verbose_name='VM Role',
+ verbose_name=_('VM Role'),
help_text=_('Virtual machines may be assigned to this role')
)
config_template = models.ForeignKey(
@@ -550,6 +557,7 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
null=True
)
name = models.CharField(
+ verbose_name=_('name'),
max_length=64,
blank=True,
null=True
@@ -563,7 +571,7 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
serial = models.CharField(
max_length=50,
blank=True,
- verbose_name='Serial number',
+ verbose_name=_('Serial number'),
help_text=_("Chassis serial number, assigned by the manufacturer")
)
asset_tag = models.CharField(
@@ -571,7 +579,7 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
blank=True,
null=True,
unique=True,
- verbose_name='Asset tag',
+ verbose_name=_('Asset tag'),
help_text=_('A unique tag used to identify this device')
)
site = models.ForeignKey(
@@ -599,21 +607,23 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
blank=True,
null=True,
validators=[MinValueValidator(1), MaxValueValidator(RACK_U_HEIGHT_MAX + 0.5)],
- verbose_name='Position (U)',
+ verbose_name=_('Position (U)'),
help_text=_('The lowest-numbered unit occupied by the device')
)
face = models.CharField(
max_length=50,
blank=True,
choices=DeviceFaceChoices,
- verbose_name='Rack face'
+ verbose_name=_('Rack face')
)
status = models.CharField(
+ verbose_name=_('status'),
max_length=50,
choices=DeviceStatusChoices,
default=DeviceStatusChoices.STATUS_ACTIVE
)
airflow = models.CharField(
+ verbose_name=_('airflow'),
max_length=50,
choices=DeviceAirflowChoices,
blank=True
@@ -624,7 +634,7 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
related_name='+',
blank=True,
null=True,
- verbose_name='Primary IPv4'
+ verbose_name=_('Primary IPv4')
)
primary_ip6 = models.OneToOneField(
to='ipam.IPAddress',
@@ -632,7 +642,7 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
related_name='+',
blank=True,
null=True,
- verbose_name='Primary IPv6'
+ verbose_name=_('Primary IPv6')
)
oob_ip = models.OneToOneField(
to='ipam.IPAddress',
@@ -657,12 +667,14 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
null=True
)
vc_position = models.PositiveSmallIntegerField(
+ verbose_name=_('vc position'),
blank=True,
null=True,
validators=[MaxValueValidator(255)],
help_text=_('Virtual chassis position')
)
vc_priority = models.PositiveSmallIntegerField(
+ verbose_name=_('vc priority'),
blank=True,
null=True,
validators=[MaxValueValidator(255)],
@@ -676,6 +688,7 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
null=True
)
latitude = models.DecimalField(
+ verbose_name=_('latitude'),
max_digits=8,
decimal_places=6,
blank=True,
@@ -683,6 +696,7 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
help_text=_("GPS coordinate in decimal format (xx.yyyyyy)")
)
longitude = models.DecimalField(
+ verbose_name=_('longitude'),
max_digits=9,
decimal_places=6,
blank=True,
@@ -763,7 +777,7 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
Lower('name'), 'site',
name='%(app_label)s_%(class)s_unique_name_site',
condition=Q(tenant__isnull=True),
- violation_error_message="Device name must be unique per site."
+ violation_error_message=_("Device name must be unique per site.")
),
models.UniqueConstraint(
fields=('rack', 'position', 'face'),
@@ -799,42 +813,42 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
# Validate site/location/rack combination
if self.rack and self.site != self.rack.site:
raise ValidationError({
- 'rack': f"Rack {self.rack} does not belong to site {self.site}.",
+ 'rack': _("Rack {rack} does not belong to site {site}.").format(rack=self.rack, site=self.site),
})
if self.location and self.site != self.location.site:
raise ValidationError({
- 'location': f"Location {self.location} does not belong to site {self.site}.",
+ 'location': _("Location {location} does not belong to site {site}.").format(location=self.location, site=self.site),
})
if self.rack and self.location and self.rack.location != self.location:
raise ValidationError({
- 'rack': f"Rack {self.rack} does not belong to location {self.location}.",
+ 'rack': _("Rack {rack} does not belong to location {location}.").format(rack=self.rack, location=self.location),
})
if self.rack is None:
if self.face:
raise ValidationError({
- 'face': "Cannot select a rack face without assigning a rack.",
+ 'face': _("Cannot select a rack face without assigning a rack."),
})
if self.position:
raise ValidationError({
- 'position': "Cannot select a rack position without assigning a rack.",
+ 'position': _("Cannot select a rack position without assigning a rack."),
})
# Validate rack position and face
if self.position and self.position % decimal.Decimal(0.5):
raise ValidationError({
- 'position': "Position must be in increments of 0.5 rack units."
+ 'position': _("Position must be in increments of 0.5 rack units.")
})
if self.position and not self.face:
raise ValidationError({
- 'face': "Must specify rack face when defining rack position.",
+ 'face': _("Must specify rack face when defining rack position."),
})
# Prevent 0U devices from being assigned to a specific position
if hasattr(self, 'device_type'):
if self.position and self.device_type.u_height == 0:
raise ValidationError({
- 'position': f"A U0 device type ({self.device_type}) cannot be assigned to a rack position."
+ 'position': _("A U0 device type ({device_type}) cannot be assigned to a rack position.").format(device_type=self.device_type)
})
if self.rack:
@@ -843,13 +857,13 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
# Child devices cannot be assigned to a rack face/unit
if self.device_type.is_child_device and self.face:
raise ValidationError({
- 'face': "Child device types cannot be assigned to a rack face. This is an attribute of the "
- "parent device."
+ 'face': _("Child device types cannot be assigned to a rack face. This is an attribute of the "
+ "parent device.")
})
if self.device_type.is_child_device and self.position:
raise ValidationError({
- 'position': "Child device types cannot be assigned to a rack position. This is an attribute of "
- "the parent device."
+ 'position': _("Child device types cannot be assigned to a rack position. This is an attribute of "
+ "the parent device.")
})
# Validate rack space
@@ -860,8 +874,9 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
)
if self.position and self.position not in available_units:
raise ValidationError({
- 'position': f"U{self.position} is already occupied or does not have sufficient space to "
- f"accommodate this device type: {self.device_type} ({self.device_type.u_height}U)"
+ 'position': _("U{position} is already occupied or does not have sufficient space to "
+ "accommodate this device type: {device_type} ({u_height}U)").format(
+ position=self.position, device_type=self.device_type, u_height=self.device_type.u_height)
})
except DeviceType.DoesNotExist:
@@ -872,7 +887,7 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
if self.primary_ip4:
if self.primary_ip4.family != 4:
raise ValidationError({
- 'primary_ip4': f"{self.primary_ip4} is not an IPv4 address."
+ 'primary_ip4': _("{primary_ip4} is not an IPv4 address.").format(primary_ip4=self.primary_ip4)
})
if self.primary_ip4.assigned_object in vc_interfaces:
pass
@@ -880,12 +895,12 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
pass
else:
raise ValidationError({
- 'primary_ip4': f"The specified IP address ({self.primary_ip4}) is not assigned to this device."
+ 'primary_ip4': _("The specified IP address ({primary_ip4}) is not assigned to this device.").format(primary_ip4=self.primary_ip4)
})
if self.primary_ip6:
if self.primary_ip6.family != 6:
raise ValidationError({
- 'primary_ip6': f"{self.primary_ip6} is not an IPv6 address."
+ 'primary_ip6': _("{primary_ip6} is not an IPv6 address.").format(primary_ip6=self.primary_ip6m)
})
if self.primary_ip6.assigned_object in vc_interfaces:
pass
@@ -893,7 +908,7 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
pass
else:
raise ValidationError({
- 'primary_ip6': f"The specified IP address ({self.primary_ip6}) is not assigned to this device."
+ 'primary_ip6': _("The specified IP address ({primary_ip6}) is not assigned to this device.").format(primary_ip6=self.primary_ip6)
})
if self.oob_ip:
if self.oob_ip.assigned_object in vc_interfaces:
@@ -909,20 +924,21 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
if hasattr(self, 'device_type') and self.platform:
if self.platform.manufacturer and self.platform.manufacturer != self.device_type.manufacturer:
raise ValidationError({
- 'platform': f"The assigned platform is limited to {self.platform.manufacturer} device types, but "
- f"this device's type belongs to {self.device_type.manufacturer}."
+ 'platform': _("The assigned platform is limited to {platform_manufacturer} device types, but "
+ "this device's type belongs to {device_type_manufacturer}.").format(
+ platform_manufacturer=self.platform.manufacturer, device_type_manufacturer=self.device_type.manufacturer)
})
# A Device can only be assigned to a Cluster in the same Site (or no Site)
if self.cluster and self.cluster.site is not None and self.cluster.site != self.site:
raise ValidationError({
- 'cluster': "The assigned cluster belongs to a different site ({})".format(self.cluster.site)
+ 'cluster': _("The assigned cluster belongs to a different site ({})").format(self.cluster.site)
})
# Validate virtual chassis assignment
if self.virtual_chassis and self.vc_position is None:
raise ValidationError({
- 'vc_position': "A device assigned to a virtual chassis must have its position defined."
+ 'vc_position': _("A device assigned to a virtual chassis must have its position defined.")
})
def _instantiate_components(self, queryset, bulk_create=True):
@@ -1107,6 +1123,7 @@ class Module(PrimaryModel, ConfigContextModel):
related_name='instances'
)
status = models.CharField(
+ verbose_name=_('status'),
max_length=50,
choices=ModuleStatusChoices,
default=ModuleStatusChoices.STATUS_ACTIVE
@@ -1114,14 +1131,14 @@ class Module(PrimaryModel, ConfigContextModel):
serial = models.CharField(
max_length=50,
blank=True,
- verbose_name='Serial number'
+ verbose_name=_('Serial number')
)
asset_tag = models.CharField(
max_length=50,
blank=True,
null=True,
unique=True,
- verbose_name='Asset tag',
+ verbose_name=_('Asset tag'),
help_text=_('A unique tag used to identify this device')
)
@@ -1144,7 +1161,7 @@ class Module(PrimaryModel, ConfigContextModel):
if hasattr(self, "module_bay") and (self.module_bay.device != self.device):
raise ValidationError(
- f"Module must be installed within a module bay belonging to the assigned device ({self.device})."
+ _("Module must be installed within a module bay belonging to the assigned device ({device}).").format(device=self.device)
)
def save(self, *args, **kwargs):
@@ -1242,9 +1259,11 @@ class VirtualChassis(PrimaryModel):
null=True
)
name = models.CharField(
+ verbose_name=_('name'),
max_length=64
)
domain = models.CharField(
+ verbose_name=_('domain'),
max_length=30,
blank=True
)
@@ -1272,7 +1291,7 @@ class VirtualChassis(PrimaryModel):
# VirtualChassis.)
if self.pk and self.master and self.master not in self.members.all():
raise ValidationError({
- 'master': f"The selected master ({self.master}) is not assigned to this virtual chassis."
+ 'master': _("The selected master ({master}) is not assigned to this virtual chassis.").format(master=self.master)
})
def delete(self, *args, **kwargs):
@@ -1286,8 +1305,8 @@ class VirtualChassis(PrimaryModel):
)
if interfaces:
raise ProtectedError(
- f"Unable to delete virtual chassis {self}. There are member interfaces which form a cross-chassis LAG",
- interfaces
+ _("Unable to delete virtual chassis {self}. There are member interfaces which form a cross-chassis LAG interfaces").format(
+ self=self, interfaces=InterfaceSpeedChoices),
)
return super().delete(*args, **kwargs)
@@ -1302,14 +1321,17 @@ class VirtualDeviceContext(PrimaryModel):
null=True
)
name = models.CharField(
+ verbose_name=_('name'),
max_length=64
)
status = models.CharField(
+ verbose_name=_('status'),
max_length=50,
choices=VirtualDeviceContextStatusChoices,
)
identifier = models.PositiveSmallIntegerField(
- help_text='Numeric identifier unique to the parent device',
+ verbose_name=_('identifier'),
+ help_text=_('Numeric identifier unique to the parent device'),
blank=True,
null=True,
)
@@ -1319,7 +1341,7 @@ class VirtualDeviceContext(PrimaryModel):
related_name='+',
blank=True,
null=True,
- verbose_name='Primary IPv4'
+ verbose_name=_('Primary IPv4')
)
primary_ip6 = models.OneToOneField(
to='ipam.IPAddress',
@@ -1327,7 +1349,7 @@ class VirtualDeviceContext(PrimaryModel):
related_name='+',
blank=True,
null=True,
- verbose_name='Primary IPv6'
+ verbose_name=_('Primary IPv6')
)
tenant = models.ForeignKey(
to='tenancy.Tenant',
@@ -1337,6 +1359,7 @@ class VirtualDeviceContext(PrimaryModel):
null=True
)
comments = models.TextField(
+ verbose_name=_('comment'),
blank=True
)
@@ -1382,7 +1405,7 @@ class VirtualDeviceContext(PrimaryModel):
continue
if primary_ip.family != family:
raise ValidationError({
- f'primary_ip{family}': f"{primary_ip} is not an IPv{family} address."
+ f'primary_ip{family}': _("{primary_ip} is not an IPv{family} address.").format(family=family, primary_ip=primary_ip)
})
device_interfaces = self.device.vc_interfaces(if_master=False)
if primary_ip.assigned_object not in device_interfaces:
diff --git a/netbox/dcim/models/mixins.py b/netbox/dcim/models/mixins.py
index 486945b0f..882b3d96b 100644
--- a/netbox/dcim/models/mixins.py
+++ b/netbox/dcim/models/mixins.py
@@ -1,17 +1,20 @@
from django.core.exceptions import ValidationError
from django.db import models
+from django.utils.translation import gettext_lazy as _
from dcim.choices import *
from utilities.utils import to_grams
class WeightMixin(models.Model):
weight = models.DecimalField(
+ verbose_name=_('weight'),
max_digits=8,
decimal_places=2,
blank=True,
null=True
)
weight_unit = models.CharField(
+ verbose_name=_('weight_unit'),
max_length=50,
choices=WeightUnitChoices,
blank=True,
@@ -40,4 +43,4 @@ class WeightMixin(models.Model):
# Validate weight and weight_unit
if self.weight and not self.weight_unit:
- raise ValidationError("Must specify a unit when setting a weight")
+ raise ValidationError(_("Must specify a unit when setting a weight"))
diff --git a/netbox/dcim/models/power.py b/netbox/dcim/models/power.py
index 3377a9edb..d90b977f5 100644
--- a/netbox/dcim/models/power.py
+++ b/netbox/dcim/models/power.py
@@ -3,7 +3,7 @@ from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.urls import reverse
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
from dcim.choices import *
from netbox.config import ConfigItem
@@ -36,6 +36,7 @@ class PowerPanel(PrimaryModel):
null=True
)
name = models.CharField(
+ verbose_name=_('name'),
max_length=100
)
@@ -72,7 +73,8 @@ class PowerPanel(PrimaryModel):
# Location must belong to assigned Site
if self.location and self.location.site != self.site:
raise ValidationError(
- f"Location {self.location} ({self.location.site}) is in a different site than {self.site}"
+ _("Location {location} ({location_site}) is in a different site than {site}").format(
+ location=self.location, location_site=self.location.site, site=self.site)
)
@@ -92,42 +94,51 @@ class PowerFeed(PrimaryModel, PathEndpoint, CabledObjectModel):
null=True
)
name = models.CharField(
+ verbose_name=_('name'),
max_length=100
)
status = models.CharField(
+ verbose_name=_('status'),
max_length=50,
choices=PowerFeedStatusChoices,
default=PowerFeedStatusChoices.STATUS_ACTIVE
)
type = models.CharField(
+ verbose_name=_('type'),
max_length=50,
choices=PowerFeedTypeChoices,
default=PowerFeedTypeChoices.TYPE_PRIMARY
)
supply = models.CharField(
+ verbose_name=_('supply'),
max_length=50,
choices=PowerFeedSupplyChoices,
default=PowerFeedSupplyChoices.SUPPLY_AC
)
phase = models.CharField(
+ verbose_name=_('phase'),
max_length=50,
choices=PowerFeedPhaseChoices,
default=PowerFeedPhaseChoices.PHASE_SINGLE
)
voltage = models.SmallIntegerField(
+ verbose_name=_('voltage'),
default=ConfigItem('POWERFEED_DEFAULT_VOLTAGE'),
validators=[ExclusionValidator([0])]
)
amperage = models.PositiveSmallIntegerField(
+ verbose_name=_('amperage'),
validators=[MinValueValidator(1)],
default=ConfigItem('POWERFEED_DEFAULT_AMPERAGE')
)
max_utilization = models.PositiveSmallIntegerField(
+ verbose_name=_('max utilization'),
validators=[MinValueValidator(1), MaxValueValidator(100)],
default=ConfigItem('POWERFEED_DEFAULT_MAX_UTILIZATION'),
help_text=_("Maximum permissible draw (percentage)")
)
available_power = models.PositiveIntegerField(
+ verbose_name=_('available power'),
default=0,
editable=False
)
@@ -160,14 +171,14 @@ class PowerFeed(PrimaryModel, PathEndpoint, CabledObjectModel):
# Rack must belong to same Site as PowerPanel
if self.rack and self.rack.site != self.power_panel.site:
- raise ValidationError("Rack {} ({}) and power panel {} ({}) are in different sites".format(
+ raise ValidationError(_("Rack {} ({}) and power panel {} ({}) are in different sites").format(
self.rack, self.rack.site, self.power_panel, self.power_panel.site
))
# AC voltage cannot be negative
if self.voltage < 0 and self.supply == PowerFeedSupplyChoices.SUPPLY_AC:
raise ValidationError({
- "voltage": "Voltage cannot be negative for AC supply"
+ "voltage": _("Voltage cannot be negative for AC supply")
})
def save(self, *args, **kwargs):
diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py
index 6d3c15eee..2014e7a35 100644
--- a/netbox/dcim/models/racks.py
+++ b/netbox/dcim/models/racks.py
@@ -9,7 +9,7 @@ from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.models import Count
from django.urls import reverse
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
from dcim.choices import *
from dcim.constants import *
@@ -39,6 +39,7 @@ class RackRole(OrganizationalModel):
Racks can be organized by functional role, similar to Devices.
"""
color = ColorField(
+ verbose_name=_('color'),
default=ColorChoices.COLOR_GREY
)
@@ -52,6 +53,7 @@ class Rack(PrimaryModel, WeightMixin):
Each Rack is assigned to a Site and (optionally) a Location.
"""
name = models.CharField(
+ verbose_name=_('name'),
max_length=100
)
_name = NaturalOrderingField(
@@ -63,7 +65,7 @@ class Rack(PrimaryModel, WeightMixin):
max_length=50,
blank=True,
null=True,
- verbose_name='Facility ID',
+ verbose_name=_('Facility ID'),
help_text=_("Locally-assigned identifier")
)
site = models.ForeignKey(
@@ -86,6 +88,7 @@ class Rack(PrimaryModel, WeightMixin):
null=True
)
status = models.CharField(
+ verbose_name=_('status'),
max_length=50,
choices=RackStatusChoices,
default=RackStatusChoices.STATUS_ACTIVE
@@ -101,60 +104,64 @@ class Rack(PrimaryModel, WeightMixin):
serial = models.CharField(
max_length=50,
blank=True,
- verbose_name='Serial number'
+ verbose_name=_('Serial number')
)
asset_tag = models.CharField(
max_length=50,
blank=True,
null=True,
unique=True,
- verbose_name='Asset tag',
+ verbose_name=_('Asset tag'),
help_text=_('A unique tag used to identify this rack')
)
type = models.CharField(
choices=RackTypeChoices,
max_length=50,
blank=True,
- verbose_name='Type'
+ verbose_name=_('Type')
)
width = models.PositiveSmallIntegerField(
choices=RackWidthChoices,
default=RackWidthChoices.WIDTH_19IN,
- verbose_name='Width',
+ verbose_name=_('Width'),
help_text=_('Rail-to-rail width')
)
u_height = models.PositiveSmallIntegerField(
default=RACK_U_HEIGHT_DEFAULT,
- verbose_name='Height (U)',
+ verbose_name=_('Height (U)'),
validators=[MinValueValidator(1), MaxValueValidator(RACK_U_HEIGHT_MAX)],
help_text=_('Height in rack units')
)
starting_unit = models.PositiveSmallIntegerField(
default=RACK_STARTING_UNIT_DEFAULT,
- verbose_name='Starting unit',
+ verbose_name=_('Starting unit'),
help_text=_('Starting unit for rack')
)
desc_units = models.BooleanField(
default=False,
- verbose_name='Descending units',
+ verbose_name=_('Descending units'),
help_text=_('Units are numbered top-to-bottom')
)
outer_width = models.PositiveSmallIntegerField(
+ verbose_name=_('outer width'),
blank=True,
null=True,
help_text=_('Outer dimension of rack (width)')
)
outer_depth = models.PositiveSmallIntegerField(
+ verbose_name=_('outer depth'),
blank=True,
null=True,
help_text=_('Outer dimension of rack (depth)')
)
outer_unit = models.CharField(
+ verbose_name=_('outer unit'),
max_length=50,
choices=RackDimensionUnitChoices,
blank=True,
)
max_weight = models.PositiveIntegerField(
+ verbose_name=_('max weight'),
blank=True,
null=True,
help_text=_('Maximum load capacity for the rack')
@@ -165,6 +172,7 @@ class Rack(PrimaryModel, WeightMixin):
null=True
)
mounting_depth = models.PositiveSmallIntegerField(
+ verbose_name=_('mounting depth'),
blank=True,
null=True,
help_text=(
@@ -222,15 +230,15 @@ class Rack(PrimaryModel, WeightMixin):
# Validate location/site assignment
if self.site and self.location and self.location.site != self.site:
- raise ValidationError(f"Assigned location must belong to parent site ({self.site}).")
+ raise ValidationError(_("Assigned location must belong to parent site ({site}).").format(site=self.site))
# Validate outer dimensions and unit
if (self.outer_width is not None or self.outer_depth is not None) and not self.outer_unit:
- raise ValidationError("Must specify a unit when setting an outer width/depth")
+ raise ValidationError(_("Must specify a unit when setting an outer width/depth"))
# Validate max_weight and weight_unit
if self.max_weight and not self.weight_unit:
- raise ValidationError("Must specify a unit when setting a maximum weight")
+ raise ValidationError(_("Must specify a unit when setting a maximum weight"))
if self.pk:
mounted_devices = Device.objects.filter(rack=self).exclude(position__isnull=True).order_by('position')
@@ -240,22 +248,22 @@ class Rack(PrimaryModel, WeightMixin):
min_height = top_device.position + top_device.device_type.u_height - self.starting_unit
if self.u_height < min_height:
raise ValidationError({
- 'u_height': f"Rack must be at least {min_height}U tall to house currently installed devices."
+ 'u_height': _("Rack must be at least {min_height}U tall to house currently installed devices.").format(min_height=min_height)
})
# Validate that the Rack's starting unit is less than or equal to the position of the lowest mounted Device
if last_device := mounted_devices.first():
if self.starting_unit > last_device.position:
raise ValidationError({
- 'starting_unit': f"Rack unit numbering must begin at {last_device.position} or less to house "
- f"currently installed devices."
+ 'starting_unit': _("Rack unit numbering must begin at {position} or less to house "
+ "currently installed devices.").format(position=last_device.position)
})
# Validate that Rack was assigned a Location of its same site, if applicable
if self.location:
if self.location.site != self.site:
raise ValidationError({
- 'location': f"Location must be from the same site, {self.site}."
+ 'location': _("Location must be from the same site, {site}.").format(site=self.site)
})
def save(self, *args, **kwargs):
@@ -504,6 +512,7 @@ class RackReservation(PrimaryModel):
related_name='reservations'
)
units = ArrayField(
+ verbose_name=_('units'),
base_field=models.PositiveSmallIntegerField()
)
tenant = models.ForeignKey(
@@ -518,6 +527,7 @@ class RackReservation(PrimaryModel):
on_delete=models.PROTECT
)
description = models.CharField(
+ verbose_name=_('description'),
max_length=200
)
@@ -544,7 +554,7 @@ class RackReservation(PrimaryModel):
invalid_units = [u for u in self.units if u not in self.rack.units]
if invalid_units:
raise ValidationError({
- 'units': "Invalid unit(s) for {}U rack: {}".format(
+ 'units': _("Invalid unit(s) for {}U rack: {}").format(
self.rack.u_height,
', '.join([str(u) for u in invalid_units]),
),
@@ -557,7 +567,7 @@ class RackReservation(PrimaryModel):
conflicting_units = [u for u in self.units if u in reserved_units]
if conflicting_units:
raise ValidationError({
- 'units': 'The following units have already been reserved: {}'.format(
+ 'units': _('The following units have already been reserved: {}').format(
', '.join([str(u) for u in conflicting_units]),
)
})
diff --git a/netbox/dcim/models/sites.py b/netbox/dcim/models/sites.py
index 3bd434648..5a9945e8b 100644
--- a/netbox/dcim/models/sites.py
+++ b/netbox/dcim/models/sites.py
@@ -2,7 +2,7 @@ from django.contrib.contenttypes.fields import GenericRelation
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
from timezone_field import TimeZoneField
from dcim.choices import *
@@ -49,7 +49,7 @@ class Region(NestedGroupModel):
fields=('name',),
name='%(app_label)s_%(class)s_name',
condition=Q(parent__isnull=True),
- violation_error_message="A top-level region with this name already exists."
+ violation_error_message=_("A top-level region with this name already exists.")
),
models.UniqueConstraint(
fields=('parent', 'slug'),
@@ -59,7 +59,7 @@ class Region(NestedGroupModel):
fields=('slug',),
name='%(app_label)s_%(class)s_slug',
condition=Q(parent__isnull=True),
- violation_error_message="A top-level region with this slug already exists."
+ violation_error_message=_("A top-level region with this slug already exists.")
),
)
@@ -104,7 +104,7 @@ class SiteGroup(NestedGroupModel):
fields=('name',),
name='%(app_label)s_%(class)s_name',
condition=Q(parent__isnull=True),
- violation_error_message="A top-level site group with this name already exists."
+ violation_error_message=_("A top-level site group with this name already exists.")
),
models.UniqueConstraint(
fields=('parent', 'slug'),
@@ -114,7 +114,7 @@ class SiteGroup(NestedGroupModel):
fields=('slug',),
name='%(app_label)s_%(class)s_slug',
condition=Q(parent__isnull=True),
- violation_error_message="A top-level site group with this slug already exists."
+ violation_error_message=_("A top-level site group with this slug already exists.")
),
)
@@ -138,6 +138,7 @@ class Site(PrimaryModel):
field can be used to include an external designation, such as a data center name (e.g. Equinix SV6).
"""
name = models.CharField(
+ verbose_name=_('name'),
max_length=100,
unique=True,
help_text=_("Full name of the site")
@@ -148,10 +149,12 @@ class Site(PrimaryModel):
blank=True
)
slug = models.SlugField(
+ verbose_name=_('slug'),
max_length=100,
unique=True
)
status = models.CharField(
+ verbose_name=_('status'),
max_length=50,
choices=SiteStatusChoices,
default=SiteStatusChoices.STATUS_ACTIVE
@@ -180,7 +183,7 @@ class Site(PrimaryModel):
facility = models.CharField(
max_length=50,
blank=True,
- help_text=_("Local facility ID or description")
+ help_text=_('Local facility ID or description')
)
asns = models.ManyToManyField(
to='ipam.ASN',
@@ -191,28 +194,32 @@ class Site(PrimaryModel):
blank=True
)
physical_address = models.CharField(
+ verbose_name=_('physical address'),
max_length=200,
blank=True,
- help_text=_("Physical location of the building")
+ help_text=_('Physical location of the building')
)
shipping_address = models.CharField(
+ verbose_name=_('shipping address'),
max_length=200,
blank=True,
- help_text=_("If different from the physical address")
+ help_text=_('If different from the physical address')
)
latitude = models.DecimalField(
+ verbose_name=_('latitude'),
max_digits=8,
decimal_places=6,
blank=True,
null=True,
- help_text=_("GPS coordinate in decimal format (xx.yyyyyy)")
+ help_text=_('GPS coordinate in decimal format (xx.yyyyyy)')
)
longitude = models.DecimalField(
+ verbose_name=_('longitude'),
max_digits=9,
decimal_places=6,
blank=True,
null=True,
- help_text=_("GPS coordinate in decimal format (xx.yyyyyy)")
+ help_text=_('GPS coordinate in decimal format (xx.yyyyyy)')
)
# Generic relations
@@ -262,6 +269,7 @@ class Location(NestedGroupModel):
related_name='locations'
)
status = models.CharField(
+ verbose_name=_('status'),
max_length=50,
choices=LocationStatusChoices,
default=LocationStatusChoices.STATUS_ACTIVE
@@ -304,7 +312,7 @@ class Location(NestedGroupModel):
fields=('site', 'name'),
name='%(app_label)s_%(class)s_name',
condition=Q(parent__isnull=True),
- violation_error_message="A location with this name already exists within the specified site."
+ violation_error_message=_("A location with this name already exists within the specified site.")
),
models.UniqueConstraint(
fields=('site', 'parent', 'slug'),
@@ -314,7 +322,7 @@ class Location(NestedGroupModel):
fields=('site', 'slug'),
name='%(app_label)s_%(class)s_slug',
condition=Q(parent__isnull=True),
- violation_error_message="A location with this slug already exists within the specified site."
+ violation_error_message=_("A location with this slug already exists within the specified site.")
),
)
@@ -329,4 +337,4 @@ class Location(NestedGroupModel):
# Parent Location (if any) must belong to the same Site
if self.parent and self.parent.site != self.site:
- raise ValidationError(f"Parent location ({self.parent}) must belong to the same site ({self.site})")
+ raise ValidationError(_("Parent location ({parent}) must belong to the same site ({site})").format(parent=self.parent, site=self.site))