13132 add gettext_lazy to models

This commit is contained in:
Arthur 2023-07-11 14:22:53 +07:00 committed by Jeremy Stretch
parent 54c610130c
commit 1195efc2d1
5 changed files with 144 additions and 89 deletions

View File

@ -12,7 +12,7 @@ from django.db.models.functions import Lower
from django.db.models.signals import post_save from django.db.models.signals import post_save
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.utils.translation import gettext_lazy as _
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
@ -78,9 +78,11 @@ class DeviceType(PrimaryModel, WeightMixin):
related_name='device_types' related_name='device_types'
) )
model = models.CharField( model = models.CharField(
verbose_name=_('model'),
max_length=100 max_length=100
) )
slug = models.SlugField( slug = models.SlugField(
verbose_name=_('slug'),
max_length=100 max_length=100
) )
default_platform = models.ForeignKey( default_platform = models.ForeignKey(
@ -89,9 +91,10 @@ class DeviceType(PrimaryModel, WeightMixin):
related_name='+', related_name='+',
blank=True, blank=True,
null=True, null=True,
verbose_name='Default platform' verbose_name=_('Default platform')
) )
part_number = models.CharField( part_number = models.CharField(
verbose_name=_('part number'),
max_length=50, max_length=50,
blank=True, blank=True,
help_text=_('Discrete part number (optional)') help_text=_('Discrete part number (optional)')
@ -100,22 +103,23 @@ class DeviceType(PrimaryModel, WeightMixin):
max_digits=4, max_digits=4,
decimal_places=1, decimal_places=1,
default=1.0, default=1.0,
verbose_name='Height (U)' verbose_name=_('Height (U)')
) )
is_full_depth = models.BooleanField( is_full_depth = models.BooleanField(
default=True, default=True,
verbose_name='Is full depth', verbose_name=_('Is full depth'),
help_text=_('Device consumes both front and rear rack faces') help_text=_('Device consumes both front and rear rack faces')
) )
subdevice_role = models.CharField( subdevice_role = models.CharField(
max_length=50, max_length=50,
choices=SubdeviceRoleChoices, choices=SubdeviceRoleChoices,
blank=True, blank=True,
verbose_name='Parent/child status', verbose_name=_('Parent/child status'),
help_text=_('Parent devices house child devices in device bays. Leave blank ' help_text=_('Parent devices house child devices in device bays. Leave blank '
'if this device type is neither a parent nor a child.') 'if this device type is neither a parent nor a child.')
) )
airflow = models.CharField( airflow = models.CharField(
verbose_name=_('airflow'),
max_length=50, max_length=50,
choices=DeviceAirflowChoices, choices=DeviceAirflowChoices,
blank=True blank=True
@ -277,7 +281,7 @@ class DeviceType(PrimaryModel, WeightMixin):
# U height must be divisible by 0.5 # U height must be divisible by 0.5
if self.u_height % decimal.Decimal(0.5): if self.u_height % decimal.Decimal(0.5):
raise ValidationError({ 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 # 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: if d.position not in u_available:
raise ValidationError({ raise ValidationError({
'u_height': "Device {} in rack {} does not have sufficient space to accommodate a height of " 'u_height': _("Device {} in rack {} does not have sufficient space to accommodate a height of "
"{}U".format(d, d.rack, self.u_height) "{}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. # 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}" url = f"{reverse('dcim:device_list')}?manufactuer_id={self.manufacturer_id}&device_type_id={self.pk}"
raise ValidationError({ raise ValidationError({
'u_height': mark_safe( 'u_height': mark_safe(
f'Unable to set 0U height: Found <a href="{url}">{racked_instance_count} instances</a> already ' _('Unable to set 0U height: Found <a href="{url}">{racked_instance_count} instances</a> already '
f'mounted within racks.' '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 self.subdevice_role != SubdeviceRoleChoices.ROLE_PARENT
) and self.pk and self.devicebaytemplates.count(): ) and self.pk and self.devicebaytemplates.count():
raise ValidationError({ raise ValidationError({
'subdevice_role': "Must delete all device bay templates associated with this device before " 'subdevice_role': _("Must delete all device bay templates associated with this device before "
"declassifying it as a parent device." "declassifying it as a parent device.")
}) })
if self.u_height and self.subdevice_role == SubdeviceRoleChoices.ROLE_CHILD: if self.u_height and self.subdevice_role == SubdeviceRoleChoices.ROLE_CHILD:
raise ValidationError({ raise ValidationError({
'u_height': "Child device types must be 0U." 'u_height': _("Child device types must be 0U.")
}) })
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
@ -367,9 +371,11 @@ class ModuleType(PrimaryModel, WeightMixin):
related_name='module_types' related_name='module_types'
) )
model = models.CharField( model = models.CharField(
verbose_name=_('model'),
max_length=100 max_length=100
) )
part_number = models.CharField( part_number = models.CharField(
verbose_name=_('part number'),
max_length=50, max_length=50,
blank=True, blank=True,
help_text=_('Discrete part number (optional)') help_text=_('Discrete part number (optional)')
@ -454,11 +460,12 @@ class DeviceRole(OrganizationalModel):
virtual machines as well. virtual machines as well.
""" """
color = ColorField( color = ColorField(
verbose_name=_('color'),
default=ColorChoices.COLOR_GREY default=ColorChoices.COLOR_GREY
) )
vm_role = models.BooleanField( vm_role = models.BooleanField(
default=True, default=True,
verbose_name='VM Role', verbose_name=_('VM Role'),
help_text=_('Virtual machines may be assigned to this role') help_text=_('Virtual machines may be assigned to this role')
) )
config_template = models.ForeignKey( config_template = models.ForeignKey(
@ -550,6 +557,7 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
null=True null=True
) )
name = models.CharField( name = models.CharField(
verbose_name=_('name'),
max_length=64, max_length=64,
blank=True, blank=True,
null=True null=True
@ -563,7 +571,7 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
serial = models.CharField( serial = models.CharField(
max_length=50, max_length=50,
blank=True, blank=True,
verbose_name='Serial number', verbose_name=_('Serial number'),
help_text=_("Chassis serial number, assigned by the manufacturer") help_text=_("Chassis serial number, assigned by the manufacturer")
) )
asset_tag = models.CharField( asset_tag = models.CharField(
@ -571,7 +579,7 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
blank=True, blank=True,
null=True, null=True,
unique=True, unique=True,
verbose_name='Asset tag', verbose_name=_('Asset tag'),
help_text=_('A unique tag used to identify this device') help_text=_('A unique tag used to identify this device')
) )
site = models.ForeignKey( site = models.ForeignKey(
@ -599,21 +607,23 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
blank=True, blank=True,
null=True, null=True,
validators=[MinValueValidator(1), MaxValueValidator(RACK_U_HEIGHT_MAX + 0.5)], 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') help_text=_('The lowest-numbered unit occupied by the device')
) )
face = models.CharField( face = models.CharField(
max_length=50, max_length=50,
blank=True, blank=True,
choices=DeviceFaceChoices, choices=DeviceFaceChoices,
verbose_name='Rack face' verbose_name=_('Rack face')
) )
status = models.CharField( status = models.CharField(
verbose_name=_('status'),
max_length=50, max_length=50,
choices=DeviceStatusChoices, choices=DeviceStatusChoices,
default=DeviceStatusChoices.STATUS_ACTIVE default=DeviceStatusChoices.STATUS_ACTIVE
) )
airflow = models.CharField( airflow = models.CharField(
verbose_name=_('airflow'),
max_length=50, max_length=50,
choices=DeviceAirflowChoices, choices=DeviceAirflowChoices,
blank=True blank=True
@ -624,7 +634,7 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
related_name='+', related_name='+',
blank=True, blank=True,
null=True, null=True,
verbose_name='Primary IPv4' verbose_name=_('Primary IPv4')
) )
primary_ip6 = models.OneToOneField( primary_ip6 = models.OneToOneField(
to='ipam.IPAddress', to='ipam.IPAddress',
@ -632,7 +642,7 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
related_name='+', related_name='+',
blank=True, blank=True,
null=True, null=True,
verbose_name='Primary IPv6' verbose_name=_('Primary IPv6')
) )
oob_ip = models.OneToOneField( oob_ip = models.OneToOneField(
to='ipam.IPAddress', to='ipam.IPAddress',
@ -657,12 +667,14 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
null=True null=True
) )
vc_position = models.PositiveSmallIntegerField( vc_position = models.PositiveSmallIntegerField(
verbose_name=_('vc position'),
blank=True, blank=True,
null=True, null=True,
validators=[MaxValueValidator(255)], validators=[MaxValueValidator(255)],
help_text=_('Virtual chassis position') help_text=_('Virtual chassis position')
) )
vc_priority = models.PositiveSmallIntegerField( vc_priority = models.PositiveSmallIntegerField(
verbose_name=_('vc priority'),
blank=True, blank=True,
null=True, null=True,
validators=[MaxValueValidator(255)], validators=[MaxValueValidator(255)],
@ -676,6 +688,7 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
null=True null=True
) )
latitude = models.DecimalField( latitude = models.DecimalField(
verbose_name=_('latitude'),
max_digits=8, max_digits=8,
decimal_places=6, decimal_places=6,
blank=True, blank=True,
@ -683,6 +696,7 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
help_text=_("GPS coordinate in decimal format (xx.yyyyyy)") help_text=_("GPS coordinate in decimal format (xx.yyyyyy)")
) )
longitude = models.DecimalField( longitude = models.DecimalField(
verbose_name=_('longitude'),
max_digits=9, max_digits=9,
decimal_places=6, decimal_places=6,
blank=True, blank=True,
@ -763,7 +777,7 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
Lower('name'), 'site', Lower('name'), 'site',
name='%(app_label)s_%(class)s_unique_name_site', name='%(app_label)s_%(class)s_unique_name_site',
condition=Q(tenant__isnull=True), 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( models.UniqueConstraint(
fields=('rack', 'position', 'face'), fields=('rack', 'position', 'face'),
@ -799,42 +813,42 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
# Validate site/location/rack combination # Validate site/location/rack combination
if self.rack and self.site != self.rack.site: if self.rack and self.site != self.rack.site:
raise ValidationError({ 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: if self.location and self.site != self.location.site:
raise ValidationError({ 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: if self.rack and self.location and self.rack.location != self.location:
raise ValidationError({ 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.rack is None:
if self.face: if self.face:
raise ValidationError({ 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: if self.position:
raise ValidationError({ 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 # Validate rack position and face
if self.position and self.position % decimal.Decimal(0.5): if self.position and self.position % decimal.Decimal(0.5):
raise ValidationError({ 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: if self.position and not self.face:
raise ValidationError({ 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 # Prevent 0U devices from being assigned to a specific position
if hasattr(self, 'device_type'): if hasattr(self, 'device_type'):
if self.position and self.device_type.u_height == 0: if self.position and self.device_type.u_height == 0:
raise ValidationError({ 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: if self.rack:
@ -843,13 +857,13 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
# Child devices cannot be assigned to a rack face/unit # Child devices cannot be assigned to a rack face/unit
if self.device_type.is_child_device and self.face: if self.device_type.is_child_device and self.face:
raise ValidationError({ raise ValidationError({
'face': "Child device types cannot be assigned to a rack face. This is an attribute of the " 'face': _("Child device types cannot be assigned to a rack face. This is an attribute of the "
"parent device." "parent device.")
}) })
if self.device_type.is_child_device and self.position: if self.device_type.is_child_device and self.position:
raise ValidationError({ raise ValidationError({
'position': "Child device types cannot be assigned to a rack position. This is an attribute of " 'position': _("Child device types cannot be assigned to a rack position. This is an attribute of "
"the parent device." "the parent device.")
}) })
# Validate rack space # Validate rack space
@ -860,8 +874,9 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
) )
if self.position and self.position not in available_units: if self.position and self.position not in available_units:
raise ValidationError({ raise ValidationError({
'position': f"U{self.position} is already occupied or does not have sufficient space to " 'position': _("U{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)" "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: except DeviceType.DoesNotExist:
@ -872,7 +887,7 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
if self.primary_ip4: if self.primary_ip4:
if self.primary_ip4.family != 4: if self.primary_ip4.family != 4:
raise ValidationError({ 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: if self.primary_ip4.assigned_object in vc_interfaces:
pass pass
@ -880,12 +895,12 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
pass pass
else: else:
raise ValidationError({ 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:
if self.primary_ip6.family != 6: if self.primary_ip6.family != 6:
raise ValidationError({ 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: if self.primary_ip6.assigned_object in vc_interfaces:
pass pass
@ -893,7 +908,7 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
pass pass
else: else:
raise ValidationError({ 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:
if self.oob_ip.assigned_object in vc_interfaces: 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 hasattr(self, 'device_type') and self.platform:
if self.platform.manufacturer and self.platform.manufacturer != self.device_type.manufacturer: if self.platform.manufacturer and self.platform.manufacturer != self.device_type.manufacturer:
raise ValidationError({ raise ValidationError({
'platform': f"The assigned platform is limited to {self.platform.manufacturer} device types, but " 'platform': _("The assigned platform is limited to {platform_manufacturer} device types, but "
f"this device's type belongs to {self.device_type.manufacturer}." "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) # 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: if self.cluster and self.cluster.site is not None and self.cluster.site != self.site:
raise ValidationError({ 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 # Validate virtual chassis assignment
if self.virtual_chassis and self.vc_position is None: if self.virtual_chassis and self.vc_position is None:
raise ValidationError({ 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): def _instantiate_components(self, queryset, bulk_create=True):
@ -1107,6 +1123,7 @@ class Module(PrimaryModel, ConfigContextModel):
related_name='instances' related_name='instances'
) )
status = models.CharField( status = models.CharField(
verbose_name=_('status'),
max_length=50, max_length=50,
choices=ModuleStatusChoices, choices=ModuleStatusChoices,
default=ModuleStatusChoices.STATUS_ACTIVE default=ModuleStatusChoices.STATUS_ACTIVE
@ -1114,14 +1131,14 @@ class Module(PrimaryModel, ConfigContextModel):
serial = models.CharField( serial = models.CharField(
max_length=50, max_length=50,
blank=True, blank=True,
verbose_name='Serial number' verbose_name=_('Serial number')
) )
asset_tag = models.CharField( asset_tag = models.CharField(
max_length=50, max_length=50,
blank=True, blank=True,
null=True, null=True,
unique=True, unique=True,
verbose_name='Asset tag', verbose_name=_('Asset tag'),
help_text=_('A unique tag used to identify this device') 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): if hasattr(self, "module_bay") and (self.module_bay.device != self.device):
raise ValidationError( 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): def save(self, *args, **kwargs):
@ -1242,9 +1259,11 @@ class VirtualChassis(PrimaryModel):
null=True null=True
) )
name = models.CharField( name = models.CharField(
verbose_name=_('name'),
max_length=64 max_length=64
) )
domain = models.CharField( domain = models.CharField(
verbose_name=_('domain'),
max_length=30, max_length=30,
blank=True blank=True
) )
@ -1272,7 +1291,7 @@ class VirtualChassis(PrimaryModel):
# VirtualChassis.) # VirtualChassis.)
if self.pk and self.master and self.master not in self.members.all(): if self.pk and self.master and self.master not in self.members.all():
raise ValidationError({ 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): def delete(self, *args, **kwargs):
@ -1286,8 +1305,8 @@ class VirtualChassis(PrimaryModel):
) )
if interfaces: if interfaces:
raise ProtectedError( raise ProtectedError(
f"Unable to delete virtual chassis {self}. There are member interfaces which form a cross-chassis LAG", _("Unable to delete virtual chassis {self}. There are member interfaces which form a cross-chassis LAG interfaces").format(
interfaces self=self, interfaces=InterfaceSpeedChoices),
) )
return super().delete(*args, **kwargs) return super().delete(*args, **kwargs)
@ -1302,14 +1321,17 @@ class VirtualDeviceContext(PrimaryModel):
null=True null=True
) )
name = models.CharField( name = models.CharField(
verbose_name=_('name'),
max_length=64 max_length=64
) )
status = models.CharField( status = models.CharField(
verbose_name=_('status'),
max_length=50, max_length=50,
choices=VirtualDeviceContextStatusChoices, choices=VirtualDeviceContextStatusChoices,
) )
identifier = models.PositiveSmallIntegerField( 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, blank=True,
null=True, null=True,
) )
@ -1319,7 +1341,7 @@ class VirtualDeviceContext(PrimaryModel):
related_name='+', related_name='+',
blank=True, blank=True,
null=True, null=True,
verbose_name='Primary IPv4' verbose_name=_('Primary IPv4')
) )
primary_ip6 = models.OneToOneField( primary_ip6 = models.OneToOneField(
to='ipam.IPAddress', to='ipam.IPAddress',
@ -1327,7 +1349,7 @@ class VirtualDeviceContext(PrimaryModel):
related_name='+', related_name='+',
blank=True, blank=True,
null=True, null=True,
verbose_name='Primary IPv6' verbose_name=_('Primary IPv6')
) )
tenant = models.ForeignKey( tenant = models.ForeignKey(
to='tenancy.Tenant', to='tenancy.Tenant',
@ -1337,6 +1359,7 @@ class VirtualDeviceContext(PrimaryModel):
null=True null=True
) )
comments = models.TextField( comments = models.TextField(
verbose_name=_('comment'),
blank=True blank=True
) )
@ -1382,7 +1405,7 @@ class VirtualDeviceContext(PrimaryModel):
continue continue
if primary_ip.family != family: if primary_ip.family != family:
raise ValidationError({ 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) device_interfaces = self.device.vc_interfaces(if_master=False)
if primary_ip.assigned_object not in device_interfaces: if primary_ip.assigned_object not in device_interfaces:

View File

@ -1,17 +1,20 @@
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_lazy as _
from dcim.choices import * from dcim.choices import *
from utilities.utils import to_grams from utilities.utils import to_grams
class WeightMixin(models.Model): class WeightMixin(models.Model):
weight = models.DecimalField( weight = models.DecimalField(
verbose_name=_('weight'),
max_digits=8, max_digits=8,
decimal_places=2, decimal_places=2,
blank=True, blank=True,
null=True null=True
) )
weight_unit = models.CharField( weight_unit = models.CharField(
verbose_name=_('weight_unit'),
max_length=50, max_length=50,
choices=WeightUnitChoices, choices=WeightUnitChoices,
blank=True, blank=True,
@ -40,4 +43,4 @@ class WeightMixin(models.Model):
# Validate weight and weight_unit # Validate weight and weight_unit
if self.weight and not self.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"))

View File

@ -3,7 +3,7 @@ from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from django.urls import reverse 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.choices import *
from netbox.config import ConfigItem from netbox.config import ConfigItem
@ -36,6 +36,7 @@ class PowerPanel(PrimaryModel):
null=True null=True
) )
name = models.CharField( name = models.CharField(
verbose_name=_('name'),
max_length=100 max_length=100
) )
@ -72,7 +73,8 @@ class PowerPanel(PrimaryModel):
# Location must belong to assigned Site # Location must belong to assigned Site
if self.location and self.location.site != self.site: if self.location and self.location.site != self.site:
raise ValidationError( 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 null=True
) )
name = models.CharField( name = models.CharField(
verbose_name=_('name'),
max_length=100 max_length=100
) )
status = models.CharField( status = models.CharField(
verbose_name=_('status'),
max_length=50, max_length=50,
choices=PowerFeedStatusChoices, choices=PowerFeedStatusChoices,
default=PowerFeedStatusChoices.STATUS_ACTIVE default=PowerFeedStatusChoices.STATUS_ACTIVE
) )
type = models.CharField( type = models.CharField(
verbose_name=_('type'),
max_length=50, max_length=50,
choices=PowerFeedTypeChoices, choices=PowerFeedTypeChoices,
default=PowerFeedTypeChoices.TYPE_PRIMARY default=PowerFeedTypeChoices.TYPE_PRIMARY
) )
supply = models.CharField( supply = models.CharField(
verbose_name=_('supply'),
max_length=50, max_length=50,
choices=PowerFeedSupplyChoices, choices=PowerFeedSupplyChoices,
default=PowerFeedSupplyChoices.SUPPLY_AC default=PowerFeedSupplyChoices.SUPPLY_AC
) )
phase = models.CharField( phase = models.CharField(
verbose_name=_('phase'),
max_length=50, max_length=50,
choices=PowerFeedPhaseChoices, choices=PowerFeedPhaseChoices,
default=PowerFeedPhaseChoices.PHASE_SINGLE default=PowerFeedPhaseChoices.PHASE_SINGLE
) )
voltage = models.SmallIntegerField( voltage = models.SmallIntegerField(
verbose_name=_('voltage'),
default=ConfigItem('POWERFEED_DEFAULT_VOLTAGE'), default=ConfigItem('POWERFEED_DEFAULT_VOLTAGE'),
validators=[ExclusionValidator([0])] validators=[ExclusionValidator([0])]
) )
amperage = models.PositiveSmallIntegerField( amperage = models.PositiveSmallIntegerField(
verbose_name=_('amperage'),
validators=[MinValueValidator(1)], validators=[MinValueValidator(1)],
default=ConfigItem('POWERFEED_DEFAULT_AMPERAGE') default=ConfigItem('POWERFEED_DEFAULT_AMPERAGE')
) )
max_utilization = models.PositiveSmallIntegerField( max_utilization = models.PositiveSmallIntegerField(
verbose_name=_('max utilization'),
validators=[MinValueValidator(1), MaxValueValidator(100)], validators=[MinValueValidator(1), MaxValueValidator(100)],
default=ConfigItem('POWERFEED_DEFAULT_MAX_UTILIZATION'), default=ConfigItem('POWERFEED_DEFAULT_MAX_UTILIZATION'),
help_text=_("Maximum permissible draw (percentage)") help_text=_("Maximum permissible draw (percentage)")
) )
available_power = models.PositiveIntegerField( available_power = models.PositiveIntegerField(
verbose_name=_('available power'),
default=0, default=0,
editable=False editable=False
) )
@ -160,14 +171,14 @@ class PowerFeed(PrimaryModel, PathEndpoint, CabledObjectModel):
# Rack must belong to same Site as PowerPanel # Rack must belong to same Site as PowerPanel
if self.rack and self.rack.site != self.power_panel.site: 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 self.rack, self.rack.site, self.power_panel, self.power_panel.site
)) ))
# AC voltage cannot be negative # AC voltage cannot be negative
if self.voltage < 0 and self.supply == PowerFeedSupplyChoices.SUPPLY_AC: if self.voltage < 0 and self.supply == PowerFeedSupplyChoices.SUPPLY_AC:
raise ValidationError({ raise ValidationError({
"voltage": "Voltage cannot be negative for AC supply" "voltage": _("Voltage cannot be negative for AC supply")
}) })
def save(self, *args, **kwargs): def save(self, *args, **kwargs):

View File

@ -9,7 +9,7 @@ from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from django.db.models import Count from django.db.models import Count
from django.urls import reverse 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.choices import *
from dcim.constants import * from dcim.constants import *
@ -39,6 +39,7 @@ class RackRole(OrganizationalModel):
Racks can be organized by functional role, similar to Devices. Racks can be organized by functional role, similar to Devices.
""" """
color = ColorField( color = ColorField(
verbose_name=_('color'),
default=ColorChoices.COLOR_GREY default=ColorChoices.COLOR_GREY
) )
@ -52,6 +53,7 @@ class Rack(PrimaryModel, WeightMixin):
Each Rack is assigned to a Site and (optionally) a Location. Each Rack is assigned to a Site and (optionally) a Location.
""" """
name = models.CharField( name = models.CharField(
verbose_name=_('name'),
max_length=100 max_length=100
) )
_name = NaturalOrderingField( _name = NaturalOrderingField(
@ -63,7 +65,7 @@ class Rack(PrimaryModel, WeightMixin):
max_length=50, max_length=50,
blank=True, blank=True,
null=True, null=True,
verbose_name='Facility ID', verbose_name=_('Facility ID'),
help_text=_("Locally-assigned identifier") help_text=_("Locally-assigned identifier")
) )
site = models.ForeignKey( site = models.ForeignKey(
@ -86,6 +88,7 @@ class Rack(PrimaryModel, WeightMixin):
null=True null=True
) )
status = models.CharField( status = models.CharField(
verbose_name=_('status'),
max_length=50, max_length=50,
choices=RackStatusChoices, choices=RackStatusChoices,
default=RackStatusChoices.STATUS_ACTIVE default=RackStatusChoices.STATUS_ACTIVE
@ -101,60 +104,64 @@ class Rack(PrimaryModel, WeightMixin):
serial = models.CharField( serial = models.CharField(
max_length=50, max_length=50,
blank=True, blank=True,
verbose_name='Serial number' verbose_name=_('Serial number')
) )
asset_tag = models.CharField( asset_tag = models.CharField(
max_length=50, max_length=50,
blank=True, blank=True,
null=True, null=True,
unique=True, unique=True,
verbose_name='Asset tag', verbose_name=_('Asset tag'),
help_text=_('A unique tag used to identify this rack') help_text=_('A unique tag used to identify this rack')
) )
type = models.CharField( type = models.CharField(
choices=RackTypeChoices, choices=RackTypeChoices,
max_length=50, max_length=50,
blank=True, blank=True,
verbose_name='Type' verbose_name=_('Type')
) )
width = models.PositiveSmallIntegerField( width = models.PositiveSmallIntegerField(
choices=RackWidthChoices, choices=RackWidthChoices,
default=RackWidthChoices.WIDTH_19IN, default=RackWidthChoices.WIDTH_19IN,
verbose_name='Width', verbose_name=_('Width'),
help_text=_('Rail-to-rail width') help_text=_('Rail-to-rail width')
) )
u_height = models.PositiveSmallIntegerField( u_height = models.PositiveSmallIntegerField(
default=RACK_U_HEIGHT_DEFAULT, default=RACK_U_HEIGHT_DEFAULT,
verbose_name='Height (U)', verbose_name=_('Height (U)'),
validators=[MinValueValidator(1), MaxValueValidator(RACK_U_HEIGHT_MAX)], validators=[MinValueValidator(1), MaxValueValidator(RACK_U_HEIGHT_MAX)],
help_text=_('Height in rack units') help_text=_('Height in rack units')
) )
starting_unit = models.PositiveSmallIntegerField( starting_unit = models.PositiveSmallIntegerField(
default=RACK_STARTING_UNIT_DEFAULT, default=RACK_STARTING_UNIT_DEFAULT,
verbose_name='Starting unit', verbose_name=_('Starting unit'),
help_text=_('Starting unit for rack') help_text=_('Starting unit for rack')
) )
desc_units = models.BooleanField( desc_units = models.BooleanField(
default=False, default=False,
verbose_name='Descending units', verbose_name=_('Descending units'),
help_text=_('Units are numbered top-to-bottom') help_text=_('Units are numbered top-to-bottom')
) )
outer_width = models.PositiveSmallIntegerField( outer_width = models.PositiveSmallIntegerField(
verbose_name=_('outer width'),
blank=True, blank=True,
null=True, null=True,
help_text=_('Outer dimension of rack (width)') help_text=_('Outer dimension of rack (width)')
) )
outer_depth = models.PositiveSmallIntegerField( outer_depth = models.PositiveSmallIntegerField(
verbose_name=_('outer depth'),
blank=True, blank=True,
null=True, null=True,
help_text=_('Outer dimension of rack (depth)') help_text=_('Outer dimension of rack (depth)')
) )
outer_unit = models.CharField( outer_unit = models.CharField(
verbose_name=_('outer unit'),
max_length=50, max_length=50,
choices=RackDimensionUnitChoices, choices=RackDimensionUnitChoices,
blank=True, blank=True,
) )
max_weight = models.PositiveIntegerField( max_weight = models.PositiveIntegerField(
verbose_name=_('max weight'),
blank=True, blank=True,
null=True, null=True,
help_text=_('Maximum load capacity for the rack') help_text=_('Maximum load capacity for the rack')
@ -165,6 +172,7 @@ class Rack(PrimaryModel, WeightMixin):
null=True null=True
) )
mounting_depth = models.PositiveSmallIntegerField( mounting_depth = models.PositiveSmallIntegerField(
verbose_name=_('mounting depth'),
blank=True, blank=True,
null=True, null=True,
help_text=( help_text=(
@ -222,15 +230,15 @@ class Rack(PrimaryModel, WeightMixin):
# Validate location/site assignment # Validate location/site assignment
if self.site and self.location and self.location.site != self.site: 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 # Validate outer dimensions and unit
if (self.outer_width is not None or self.outer_depth is not None) and not self.outer_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 # Validate max_weight and weight_unit
if self.max_weight and not self.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: if self.pk:
mounted_devices = Device.objects.filter(rack=self).exclude(position__isnull=True).order_by('position') 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 min_height = top_device.position + top_device.device_type.u_height - self.starting_unit
if self.u_height < min_height: if self.u_height < min_height:
raise ValidationError({ 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 # 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 last_device := mounted_devices.first():
if self.starting_unit > last_device.position: if self.starting_unit > last_device.position:
raise ValidationError({ raise ValidationError({
'starting_unit': f"Rack unit numbering must begin at {last_device.position} or less to house " 'starting_unit': _("Rack unit numbering must begin at {position} or less to house "
f"currently installed devices." "currently installed devices.").format(position=last_device.position)
}) })
# Validate that Rack was assigned a Location of its same site, if applicable # Validate that Rack was assigned a Location of its same site, if applicable
if self.location: if self.location:
if self.location.site != self.site: if self.location.site != self.site:
raise ValidationError({ 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): def save(self, *args, **kwargs):
@ -504,6 +512,7 @@ class RackReservation(PrimaryModel):
related_name='reservations' related_name='reservations'
) )
units = ArrayField( units = ArrayField(
verbose_name=_('units'),
base_field=models.PositiveSmallIntegerField() base_field=models.PositiveSmallIntegerField()
) )
tenant = models.ForeignKey( tenant = models.ForeignKey(
@ -518,6 +527,7 @@ class RackReservation(PrimaryModel):
on_delete=models.PROTECT on_delete=models.PROTECT
) )
description = models.CharField( description = models.CharField(
verbose_name=_('description'),
max_length=200 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] invalid_units = [u for u in self.units if u not in self.rack.units]
if invalid_units: if invalid_units:
raise ValidationError({ raise ValidationError({
'units': "Invalid unit(s) for {}U rack: {}".format( 'units': _("Invalid unit(s) for {}U rack: {}").format(
self.rack.u_height, self.rack.u_height,
', '.join([str(u) for u in invalid_units]), ', '.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] conflicting_units = [u for u in self.units if u in reserved_units]
if conflicting_units: if conflicting_units:
raise ValidationError({ 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]), ', '.join([str(u) for u in conflicting_units]),
) )
}) })

View File

@ -2,7 +2,7 @@ from django.contrib.contenttypes.fields import GenericRelation
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.urls import reverse 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 timezone_field import TimeZoneField
from dcim.choices import * from dcim.choices import *
@ -49,7 +49,7 @@ class Region(NestedGroupModel):
fields=('name',), fields=('name',),
name='%(app_label)s_%(class)s_name', name='%(app_label)s_%(class)s_name',
condition=Q(parent__isnull=True), 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( models.UniqueConstraint(
fields=('parent', 'slug'), fields=('parent', 'slug'),
@ -59,7 +59,7 @@ class Region(NestedGroupModel):
fields=('slug',), fields=('slug',),
name='%(app_label)s_%(class)s_slug', name='%(app_label)s_%(class)s_slug',
condition=Q(parent__isnull=True), 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',), fields=('name',),
name='%(app_label)s_%(class)s_name', name='%(app_label)s_%(class)s_name',
condition=Q(parent__isnull=True), 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( models.UniqueConstraint(
fields=('parent', 'slug'), fields=('parent', 'slug'),
@ -114,7 +114,7 @@ class SiteGroup(NestedGroupModel):
fields=('slug',), fields=('slug',),
name='%(app_label)s_%(class)s_slug', name='%(app_label)s_%(class)s_slug',
condition=Q(parent__isnull=True), 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). field can be used to include an external designation, such as a data center name (e.g. Equinix SV6).
""" """
name = models.CharField( name = models.CharField(
verbose_name=_('name'),
max_length=100, max_length=100,
unique=True, unique=True,
help_text=_("Full name of the site") help_text=_("Full name of the site")
@ -148,10 +149,12 @@ class Site(PrimaryModel):
blank=True blank=True
) )
slug = models.SlugField( slug = models.SlugField(
verbose_name=_('slug'),
max_length=100, max_length=100,
unique=True unique=True
) )
status = models.CharField( status = models.CharField(
verbose_name=_('status'),
max_length=50, max_length=50,
choices=SiteStatusChoices, choices=SiteStatusChoices,
default=SiteStatusChoices.STATUS_ACTIVE default=SiteStatusChoices.STATUS_ACTIVE
@ -180,7 +183,7 @@ class Site(PrimaryModel):
facility = models.CharField( facility = models.CharField(
max_length=50, max_length=50,
blank=True, blank=True,
help_text=_("Local facility ID or description") help_text=_('Local facility ID or description')
) )
asns = models.ManyToManyField( asns = models.ManyToManyField(
to='ipam.ASN', to='ipam.ASN',
@ -191,28 +194,32 @@ class Site(PrimaryModel):
blank=True blank=True
) )
physical_address = models.CharField( physical_address = models.CharField(
verbose_name=_('physical address'),
max_length=200, max_length=200,
blank=True, blank=True,
help_text=_("Physical location of the building") help_text=_('Physical location of the building')
) )
shipping_address = models.CharField( shipping_address = models.CharField(
verbose_name=_('shipping address'),
max_length=200, max_length=200,
blank=True, blank=True,
help_text=_("If different from the physical address") help_text=_('If different from the physical address')
) )
latitude = models.DecimalField( latitude = models.DecimalField(
verbose_name=_('latitude'),
max_digits=8, max_digits=8,
decimal_places=6, decimal_places=6,
blank=True, blank=True,
null=True, null=True,
help_text=_("GPS coordinate in decimal format (xx.yyyyyy)") help_text=_('GPS coordinate in decimal format (xx.yyyyyy)')
) )
longitude = models.DecimalField( longitude = models.DecimalField(
verbose_name=_('longitude'),
max_digits=9, max_digits=9,
decimal_places=6, decimal_places=6,
blank=True, blank=True,
null=True, null=True,
help_text=_("GPS coordinate in decimal format (xx.yyyyyy)") help_text=_('GPS coordinate in decimal format (xx.yyyyyy)')
) )
# Generic relations # Generic relations
@ -262,6 +269,7 @@ class Location(NestedGroupModel):
related_name='locations' related_name='locations'
) )
status = models.CharField( status = models.CharField(
verbose_name=_('status'),
max_length=50, max_length=50,
choices=LocationStatusChoices, choices=LocationStatusChoices,
default=LocationStatusChoices.STATUS_ACTIVE default=LocationStatusChoices.STATUS_ACTIVE
@ -304,7 +312,7 @@ class Location(NestedGroupModel):
fields=('site', 'name'), fields=('site', 'name'),
name='%(app_label)s_%(class)s_name', name='%(app_label)s_%(class)s_name',
condition=Q(parent__isnull=True), 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( models.UniqueConstraint(
fields=('site', 'parent', 'slug'), fields=('site', 'parent', 'slug'),
@ -314,7 +322,7 @@ class Location(NestedGroupModel):
fields=('site', 'slug'), fields=('site', 'slug'),
name='%(app_label)s_%(class)s_slug', name='%(app_label)s_%(class)s_slug',
condition=Q(parent__isnull=True), 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 # Parent Location (if any) must belong to the same Site
if self.parent and self.parent.site != self.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))