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