mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-20 04:12:25 -06:00
Closes #13132: Wrap verbose_name and other model text with gettext_lazy() (i18n)
--------- Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
This commit is contained in:
@@ -8,6 +8,7 @@ from django.db import models
|
||||
from django.db.models import Sum
|
||||
from django.dispatch import Signal
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
@@ -40,11 +41,13 @@ class Cable(PrimaryModel):
|
||||
A physical connection between two endpoints.
|
||||
"""
|
||||
type = models.CharField(
|
||||
verbose_name=_('type'),
|
||||
max_length=50,
|
||||
choices=CableTypeChoices,
|
||||
blank=True
|
||||
)
|
||||
status = models.CharField(
|
||||
verbose_name=_('status'),
|
||||
max_length=50,
|
||||
choices=LinkStatusChoices,
|
||||
default=LinkStatusChoices.STATUS_CONNECTED
|
||||
@@ -57,19 +60,23 @@ class Cable(PrimaryModel):
|
||||
null=True
|
||||
)
|
||||
label = models.CharField(
|
||||
verbose_name=_('label'),
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
color = ColorField(
|
||||
verbose_name=_('color'),
|
||||
blank=True
|
||||
)
|
||||
length = models.DecimalField(
|
||||
verbose_name=_('length'),
|
||||
max_digits=8,
|
||||
decimal_places=2,
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
length_unit = models.CharField(
|
||||
verbose_name=_('length unit'),
|
||||
max_length=50,
|
||||
choices=CableLengthUnitChoices,
|
||||
blank=True,
|
||||
@@ -235,7 +242,7 @@ class CableTermination(ChangeLoggedModel):
|
||||
cable_end = models.CharField(
|
||||
max_length=1,
|
||||
choices=CableEndChoices,
|
||||
verbose_name='End'
|
||||
verbose_name=_('end')
|
||||
)
|
||||
termination_type = models.ForeignKey(
|
||||
to=ContentType,
|
||||
@@ -403,15 +410,19 @@ class CablePath(models.Model):
|
||||
`_nodes` retains a flattened list of all nodes within the path to enable simple filtering.
|
||||
"""
|
||||
path = models.JSONField(
|
||||
verbose_name=_('path'),
|
||||
default=list
|
||||
)
|
||||
is_active = models.BooleanField(
|
||||
verbose_name=_('is active'),
|
||||
default=False
|
||||
)
|
||||
is_complete = models.BooleanField(
|
||||
verbose_name=_('is complete'),
|
||||
default=False
|
||||
)
|
||||
is_split = models.BooleanField(
|
||||
verbose_name=_('is split'),
|
||||
default=False
|
||||
)
|
||||
_nodes = PathField()
|
||||
|
||||
@@ -3,7 +3,7 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
|
||||
from dcim.choices import *
|
||||
@@ -41,10 +41,11 @@ class ComponentTemplateModel(ChangeLoggedModel, TrackingModelMixin):
|
||||
related_name='%(class)ss'
|
||||
)
|
||||
name = models.CharField(
|
||||
verbose_name=_('name'),
|
||||
max_length=64,
|
||||
help_text="""
|
||||
{module} is accepted as a substitution for the module bay position when attached to a module type.
|
||||
"""
|
||||
help_text=_(
|
||||
"{module} is accepted as a substitution for the module bay position when attached to a module type."
|
||||
)
|
||||
)
|
||||
_name = NaturalOrderingField(
|
||||
target_field='name',
|
||||
@@ -52,11 +53,13 @@ class ComponentTemplateModel(ChangeLoggedModel, TrackingModelMixin):
|
||||
blank=True
|
||||
)
|
||||
label = models.CharField(
|
||||
verbose_name=_('label'),
|
||||
max_length=64,
|
||||
blank=True,
|
||||
help_text=_("Physical label")
|
||||
help_text=_('Physical label')
|
||||
)
|
||||
description = models.CharField(
|
||||
verbose_name=_('description'),
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
@@ -98,7 +101,7 @@ class ComponentTemplateModel(ChangeLoggedModel, TrackingModelMixin):
|
||||
|
||||
if self.pk is not None and self._original_device_type != self.device_type_id:
|
||||
raise ValidationError({
|
||||
"device_type": "Component templates cannot be moved to a different device type."
|
||||
"device_type": _("Component templates cannot be moved to a different device type.")
|
||||
})
|
||||
|
||||
|
||||
@@ -149,11 +152,11 @@ class ModularComponentTemplateModel(ComponentTemplateModel):
|
||||
# A component template must belong to a DeviceType *or* to a ModuleType
|
||||
if self.device_type and self.module_type:
|
||||
raise ValidationError(
|
||||
"A component template cannot be associated with both a device type and a module type."
|
||||
_("A component template cannot be associated with both a device type and a module type.")
|
||||
)
|
||||
if not self.device_type and not self.module_type:
|
||||
raise ValidationError(
|
||||
"A component template must be associated with either a device type or a module type."
|
||||
_("A component template must be associated with either a device type or a module type.")
|
||||
)
|
||||
|
||||
def resolve_name(self, module):
|
||||
@@ -172,6 +175,7 @@ class ConsolePortTemplate(ModularComponentTemplateModel):
|
||||
A template for a ConsolePort to be created for a new Device.
|
||||
"""
|
||||
type = models.CharField(
|
||||
verbose_name=_('type'),
|
||||
max_length=50,
|
||||
choices=ConsolePortTypeChoices,
|
||||
blank=True
|
||||
@@ -201,6 +205,7 @@ class ConsoleServerPortTemplate(ModularComponentTemplateModel):
|
||||
A template for a ConsoleServerPort to be created for a new Device.
|
||||
"""
|
||||
type = models.CharField(
|
||||
verbose_name=_('type'),
|
||||
max_length=50,
|
||||
choices=ConsolePortTypeChoices,
|
||||
blank=True
|
||||
@@ -231,21 +236,24 @@ class PowerPortTemplate(ModularComponentTemplateModel):
|
||||
A template for a PowerPort to be created for a new Device.
|
||||
"""
|
||||
type = models.CharField(
|
||||
verbose_name=_('type'),
|
||||
max_length=50,
|
||||
choices=PowerPortTypeChoices,
|
||||
blank=True
|
||||
)
|
||||
maximum_draw = models.PositiveIntegerField(
|
||||
verbose_name=_('maximum draw'),
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[MinValueValidator(1)],
|
||||
help_text=_("Maximum power draw (watts)")
|
||||
help_text=_('Maximum power draw (watts)')
|
||||
)
|
||||
allocated_draw = models.PositiveIntegerField(
|
||||
verbose_name=_('allocated draw'),
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[MinValueValidator(1)],
|
||||
help_text=_("Allocated power draw (watts)")
|
||||
help_text=_('Allocated power draw (watts)')
|
||||
)
|
||||
|
||||
component_model = PowerPort
|
||||
@@ -267,7 +275,7 @@ class PowerPortTemplate(ModularComponentTemplateModel):
|
||||
if self.maximum_draw is not None and self.allocated_draw is not None:
|
||||
if self.allocated_draw > self.maximum_draw:
|
||||
raise ValidationError({
|
||||
'allocated_draw': f"Allocated draw cannot exceed the maximum draw ({self.maximum_draw}W)."
|
||||
'allocated_draw': _("Allocated draw cannot exceed the maximum draw ({maximum_draw}W).").format(maximum_draw=self.maximum_draw)
|
||||
})
|
||||
|
||||
def to_yaml(self):
|
||||
@@ -286,6 +294,7 @@ class PowerOutletTemplate(ModularComponentTemplateModel):
|
||||
A template for a PowerOutlet to be created for a new Device.
|
||||
"""
|
||||
type = models.CharField(
|
||||
verbose_name=_('type'),
|
||||
max_length=50,
|
||||
choices=PowerOutletTypeChoices,
|
||||
blank=True
|
||||
@@ -298,10 +307,11 @@ class PowerOutletTemplate(ModularComponentTemplateModel):
|
||||
related_name='poweroutlet_templates'
|
||||
)
|
||||
feed_leg = models.CharField(
|
||||
verbose_name=_('feed leg'),
|
||||
max_length=50,
|
||||
choices=PowerOutletFeedLegChoices,
|
||||
blank=True,
|
||||
help_text=_("Phase (for three-phase feeds)")
|
||||
help_text=_('Phase (for three-phase feeds)')
|
||||
)
|
||||
|
||||
component_model = PowerOutlet
|
||||
@@ -313,11 +323,11 @@ class PowerOutletTemplate(ModularComponentTemplateModel):
|
||||
if self.power_port:
|
||||
if self.device_type and self.power_port.device_type != self.device_type:
|
||||
raise ValidationError(
|
||||
f"Parent power port ({self.power_port}) must belong to the same device type"
|
||||
_("Parent power port ({power_port}) must belong to the same device type").format(power_port=self.power_port)
|
||||
)
|
||||
if self.module_type and self.power_port.module_type != self.module_type:
|
||||
raise ValidationError(
|
||||
f"Parent power port ({self.power_port}) must belong to the same module type"
|
||||
_("Parent power port ({power_port}) must belong to the same module type").format(power_port=self.power_port)
|
||||
)
|
||||
|
||||
def instantiate(self, **kwargs):
|
||||
@@ -359,15 +369,17 @@ class InterfaceTemplate(ModularComponentTemplateModel):
|
||||
blank=True
|
||||
)
|
||||
type = models.CharField(
|
||||
verbose_name=_('type'),
|
||||
max_length=50,
|
||||
choices=InterfaceTypeChoices
|
||||
)
|
||||
enabled = models.BooleanField(
|
||||
verbose_name=_('enabled'),
|
||||
default=True
|
||||
)
|
||||
mgmt_only = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name='Management only'
|
||||
verbose_name=_('management only')
|
||||
)
|
||||
bridge = models.ForeignKey(
|
||||
to='self',
|
||||
@@ -375,25 +387,25 @@ class InterfaceTemplate(ModularComponentTemplateModel):
|
||||
related_name='bridge_interfaces',
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name='Bridge interface'
|
||||
verbose_name=_('bridge interface')
|
||||
)
|
||||
poe_mode = models.CharField(
|
||||
max_length=50,
|
||||
choices=InterfacePoEModeChoices,
|
||||
blank=True,
|
||||
verbose_name='PoE mode'
|
||||
verbose_name=_('PoE mode')
|
||||
)
|
||||
poe_type = models.CharField(
|
||||
max_length=50,
|
||||
choices=InterfacePoETypeChoices,
|
||||
blank=True,
|
||||
verbose_name='PoE type'
|
||||
verbose_name=_('PoE type')
|
||||
)
|
||||
rf_role = models.CharField(
|
||||
max_length=30,
|
||||
choices=WirelessRoleChoices,
|
||||
blank=True,
|
||||
verbose_name='Wireless role'
|
||||
verbose_name=_('wireless role')
|
||||
)
|
||||
|
||||
component_model = Interface
|
||||
@@ -403,14 +415,14 @@ class InterfaceTemplate(ModularComponentTemplateModel):
|
||||
|
||||
if self.bridge:
|
||||
if self.pk and self.bridge_id == self.pk:
|
||||
raise ValidationError({'bridge': "An interface cannot be bridged to itself."})
|
||||
raise ValidationError({'bridge': _("An interface cannot be bridged to itself.")})
|
||||
if self.device_type and self.device_type != self.bridge.device_type:
|
||||
raise ValidationError({
|
||||
'bridge': f"Bridge interface ({self.bridge}) must belong to the same device type"
|
||||
'bridge': _("Bridge interface ({bridge}) must belong to the same device type").format(bridge=self.bridge)
|
||||
})
|
||||
if self.module_type and self.module_type != self.bridge.module_type:
|
||||
raise ValidationError({
|
||||
'bridge': f"Bridge interface ({self.bridge}) must belong to the same module type"
|
||||
'bridge': _("Bridge interface ({bridge}) must belong to the same module type").format(bridge=self.bridge)
|
||||
})
|
||||
|
||||
if self.rf_role and self.type not in WIRELESS_IFACE_TYPES:
|
||||
@@ -452,10 +464,12 @@ class FrontPortTemplate(ModularComponentTemplateModel):
|
||||
Template for a pass-through port on the front of a new Device.
|
||||
"""
|
||||
type = models.CharField(
|
||||
verbose_name=_('type'),
|
||||
max_length=50,
|
||||
choices=PortTypeChoices
|
||||
)
|
||||
color = ColorField(
|
||||
verbose_name=_('color'),
|
||||
blank=True
|
||||
)
|
||||
rear_port = models.ForeignKey(
|
||||
@@ -464,6 +478,7 @@ class FrontPortTemplate(ModularComponentTemplateModel):
|
||||
related_name='frontport_templates'
|
||||
)
|
||||
rear_port_position = models.PositiveSmallIntegerField(
|
||||
verbose_name=_('rear port position'),
|
||||
default=1,
|
||||
validators=[
|
||||
MinValueValidator(REARPORT_POSITIONS_MIN),
|
||||
@@ -497,13 +512,13 @@ class FrontPortTemplate(ModularComponentTemplateModel):
|
||||
# Validate rear port assignment
|
||||
if self.rear_port.device_type != self.device_type:
|
||||
raise ValidationError(
|
||||
"Rear port ({}) must belong to the same device type".format(self.rear_port)
|
||||
_("Rear port ({}) must belong to the same device type").format(self.rear_port)
|
||||
)
|
||||
|
||||
# Validate rear port position assignment
|
||||
if self.rear_port_position > self.rear_port.positions:
|
||||
raise ValidationError(
|
||||
"Invalid rear port position ({}); rear port {} has only {} positions".format(
|
||||
_("Invalid rear port position ({}); rear port {} has only {} positions").format(
|
||||
self.rear_port_position, self.rear_port.name, self.rear_port.positions
|
||||
)
|
||||
)
|
||||
@@ -545,13 +560,16 @@ class RearPortTemplate(ModularComponentTemplateModel):
|
||||
Template for a pass-through port on the rear of a new Device.
|
||||
"""
|
||||
type = models.CharField(
|
||||
verbose_name=_('type'),
|
||||
max_length=50,
|
||||
choices=PortTypeChoices
|
||||
)
|
||||
color = ColorField(
|
||||
verbose_name=_('color'),
|
||||
blank=True
|
||||
)
|
||||
positions = models.PositiveSmallIntegerField(
|
||||
verbose_name=_('positions'),
|
||||
default=1,
|
||||
validators=[
|
||||
MinValueValidator(REARPORT_POSITIONS_MIN),
|
||||
@@ -588,6 +606,7 @@ class ModuleBayTemplate(ComponentTemplateModel):
|
||||
A template for a ModuleBay to be created for a new parent Device.
|
||||
"""
|
||||
position = models.CharField(
|
||||
verbose_name=_('position'),
|
||||
max_length=30,
|
||||
blank=True,
|
||||
help_text=_('Identifier to reference when renaming installed components')
|
||||
@@ -630,7 +649,7 @@ class DeviceBayTemplate(ComponentTemplateModel):
|
||||
def clean(self):
|
||||
if self.device_type and self.device_type.subdevice_role != SubdeviceRoleChoices.ROLE_PARENT:
|
||||
raise ValidationError(
|
||||
f"Subdevice role of device type ({self.device_type}) must be set to \"parent\" to allow device bays."
|
||||
_("Subdevice role of device type ({device_type}) must be set to \"parent\" to allow device bays.").format(device_type=self.device_type)
|
||||
)
|
||||
|
||||
def to_yaml(self):
|
||||
@@ -685,7 +704,7 @@ class InventoryItemTemplate(MPTTModel, ComponentTemplateModel):
|
||||
)
|
||||
part_id = models.CharField(
|
||||
max_length=50,
|
||||
verbose_name='Part ID',
|
||||
verbose_name=_('part ID'),
|
||||
blank=True,
|
||||
help_text=_('Manufacturer-assigned part identifier')
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@ from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.db.models import Sum
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
|
||||
from dcim.choices import *
|
||||
@@ -52,6 +52,7 @@ class ComponentModel(NetBoxModel):
|
||||
related_name='%(class)ss'
|
||||
)
|
||||
name = models.CharField(
|
||||
verbose_name=_('name'),
|
||||
max_length=64
|
||||
)
|
||||
_name = NaturalOrderingField(
|
||||
@@ -60,11 +61,13 @@ class ComponentModel(NetBoxModel):
|
||||
blank=True
|
||||
)
|
||||
label = models.CharField(
|
||||
verbose_name=_('label'),
|
||||
max_length=64,
|
||||
blank=True,
|
||||
help_text=_("Physical label")
|
||||
help_text=_('Physical label')
|
||||
)
|
||||
description = models.CharField(
|
||||
verbose_name=_('description'),
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
@@ -101,7 +104,7 @@ class ComponentModel(NetBoxModel):
|
||||
# Check list of Modules that allow device field to be changed
|
||||
if (type(self) not in [InventoryItem]) and (self.pk is not None) and (self._original_device != self.device_id):
|
||||
raise ValidationError({
|
||||
"device": "Components cannot be moved to a different device."
|
||||
"device": _("Components cannot be moved to a different device.")
|
||||
})
|
||||
|
||||
@property
|
||||
@@ -140,13 +143,15 @@ class CabledObjectModel(models.Model):
|
||||
null=True
|
||||
)
|
||||
cable_end = models.CharField(
|
||||
verbose_name=_('cable end'),
|
||||
max_length=1,
|
||||
blank=True,
|
||||
choices=CableEndChoices
|
||||
)
|
||||
mark_connected = models.BooleanField(
|
||||
verbose_name=_('mark connected'),
|
||||
default=False,
|
||||
help_text=_("Treat as if a cable is connected")
|
||||
help_text=_('Treat as if a cable is connected')
|
||||
)
|
||||
|
||||
cable_terminations = GenericRelation(
|
||||
@@ -164,15 +169,15 @@ class CabledObjectModel(models.Model):
|
||||
|
||||
if self.cable and not self.cable_end:
|
||||
raise ValidationError({
|
||||
"cable_end": "Must specify cable end (A or B) when attaching a cable."
|
||||
"cable_end": _("Must specify cable end (A or B) when attaching a cable.")
|
||||
})
|
||||
if self.cable_end and not self.cable:
|
||||
raise ValidationError({
|
||||
"cable_end": "Cable end must not be set without a cable."
|
||||
"cable_end": _("Cable end must not be set without a cable.")
|
||||
})
|
||||
if self.mark_connected and self.cable:
|
||||
raise ValidationError({
|
||||
"mark_connected": "Cannot mark as connected with a cable attached."
|
||||
"mark_connected": _("Cannot mark as connected with a cable attached.")
|
||||
})
|
||||
|
||||
@property
|
||||
@@ -195,7 +200,9 @@ class CabledObjectModel(models.Model):
|
||||
|
||||
@property
|
||||
def parent_object(self):
|
||||
raise NotImplementedError(f"{self.__class__.__name__} models must declare a parent_object property")
|
||||
raise NotImplementedError(
|
||||
_("{class_name} models must declare a parent_object property").format(class_name=self.__class__.__name__)
|
||||
)
|
||||
|
||||
@property
|
||||
def opposite_cable_end(self):
|
||||
@@ -275,12 +282,14 @@ class ConsolePort(ModularComponentModel, CabledObjectModel, PathEndpoint, Tracki
|
||||
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
|
||||
"""
|
||||
type = models.CharField(
|
||||
verbose_name=_('type'),
|
||||
max_length=50,
|
||||
choices=ConsolePortTypeChoices,
|
||||
blank=True,
|
||||
help_text=_('Physical port type')
|
||||
)
|
||||
speed = models.PositiveIntegerField(
|
||||
verbose_name=_('speed'),
|
||||
choices=ConsolePortSpeedChoices,
|
||||
blank=True,
|
||||
null=True,
|
||||
@@ -298,12 +307,14 @@ class ConsoleServerPort(ModularComponentModel, CabledObjectModel, PathEndpoint,
|
||||
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
|
||||
"""
|
||||
type = models.CharField(
|
||||
verbose_name=_('type'),
|
||||
max_length=50,
|
||||
choices=ConsolePortTypeChoices,
|
||||
blank=True,
|
||||
help_text=_('Physical port type')
|
||||
)
|
||||
speed = models.PositiveIntegerField(
|
||||
verbose_name=_('speed'),
|
||||
choices=ConsolePortSpeedChoices,
|
||||
blank=True,
|
||||
null=True,
|
||||
@@ -325,22 +336,25 @@ class PowerPort(ModularComponentModel, CabledObjectModel, PathEndpoint, Tracking
|
||||
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
|
||||
"""
|
||||
type = models.CharField(
|
||||
verbose_name=_('type'),
|
||||
max_length=50,
|
||||
choices=PowerPortTypeChoices,
|
||||
blank=True,
|
||||
help_text=_('Physical port type')
|
||||
)
|
||||
maximum_draw = models.PositiveIntegerField(
|
||||
verbose_name=_('maximum draw'),
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[MinValueValidator(1)],
|
||||
help_text=_("Maximum power draw (watts)")
|
||||
)
|
||||
allocated_draw = models.PositiveIntegerField(
|
||||
verbose_name=_('allocated draw'),
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[MinValueValidator(1)],
|
||||
help_text=_("Allocated power draw (watts)")
|
||||
help_text=_('Allocated power draw (watts)')
|
||||
)
|
||||
|
||||
clone_fields = ('device', 'module', 'maximum_draw', 'allocated_draw')
|
||||
@@ -354,7 +368,9 @@ class PowerPort(ModularComponentModel, CabledObjectModel, PathEndpoint, Tracking
|
||||
if self.maximum_draw is not None and self.allocated_draw is not None:
|
||||
if self.allocated_draw > self.maximum_draw:
|
||||
raise ValidationError({
|
||||
'allocated_draw': f"Allocated draw cannot exceed the maximum draw ({self.maximum_draw}W)."
|
||||
'allocated_draw': _(
|
||||
"Allocated draw cannot exceed the maximum draw ({maximum_draw}W)."
|
||||
).format(maximum_draw=self.maximum_draw)
|
||||
})
|
||||
|
||||
def get_downstream_powerports(self, leg=None):
|
||||
@@ -434,6 +450,7 @@ class PowerOutlet(ModularComponentModel, CabledObjectModel, PathEndpoint, Tracki
|
||||
A physical power outlet (output) within a Device which provides power to a PowerPort.
|
||||
"""
|
||||
type = models.CharField(
|
||||
verbose_name=_('type'),
|
||||
max_length=50,
|
||||
choices=PowerOutletTypeChoices,
|
||||
blank=True,
|
||||
@@ -447,10 +464,11 @@ class PowerOutlet(ModularComponentModel, CabledObjectModel, PathEndpoint, Tracki
|
||||
related_name='poweroutlets'
|
||||
)
|
||||
feed_leg = models.CharField(
|
||||
verbose_name=_('feed leg'),
|
||||
max_length=50,
|
||||
choices=PowerOutletFeedLegChoices,
|
||||
blank=True,
|
||||
help_text=_("Phase (for three-phase feeds)")
|
||||
help_text=_('Phase (for three-phase feeds)')
|
||||
)
|
||||
|
||||
clone_fields = ('device', 'module', 'type', 'power_port', 'feed_leg')
|
||||
@@ -463,7 +481,9 @@ class PowerOutlet(ModularComponentModel, CabledObjectModel, PathEndpoint, Tracki
|
||||
|
||||
# Validate power port assignment
|
||||
if self.power_port and self.power_port.device != self.device:
|
||||
raise ValidationError(f"Parent power port ({self.power_port}) must belong to the same device")
|
||||
raise ValidationError(
|
||||
_("Parent power port ({power_port}) must belong to the same device").format(power_port=self.power_port)
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@@ -475,12 +495,13 @@ class BaseInterface(models.Model):
|
||||
Abstract base class for fields shared by dcim.Interface and virtualization.VMInterface.
|
||||
"""
|
||||
enabled = models.BooleanField(
|
||||
verbose_name=_('enabled'),
|
||||
default=True
|
||||
)
|
||||
mac_address = MACAddressField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name='MAC Address'
|
||||
verbose_name=_('MAC address')
|
||||
)
|
||||
mtu = models.PositiveIntegerField(
|
||||
blank=True,
|
||||
@@ -489,13 +510,14 @@ class BaseInterface(models.Model):
|
||||
MinValueValidator(INTERFACE_MTU_MIN),
|
||||
MaxValueValidator(INTERFACE_MTU_MAX)
|
||||
],
|
||||
verbose_name='MTU'
|
||||
verbose_name=_('MTU')
|
||||
)
|
||||
mode = models.CharField(
|
||||
verbose_name=_('mode'),
|
||||
max_length=50,
|
||||
choices=InterfaceModeChoices,
|
||||
blank=True,
|
||||
help_text=_("IEEE 802.1Q tagging strategy")
|
||||
help_text=_('IEEE 802.1Q tagging strategy')
|
||||
)
|
||||
parent = models.ForeignKey(
|
||||
to='self',
|
||||
@@ -503,7 +525,7 @@ class BaseInterface(models.Model):
|
||||
related_name='child_interfaces',
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name='Parent interface'
|
||||
verbose_name=_('parent interface')
|
||||
)
|
||||
bridge = models.ForeignKey(
|
||||
to='self',
|
||||
@@ -511,7 +533,7 @@ class BaseInterface(models.Model):
|
||||
related_name='bridge_interfaces',
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name='Bridge interface'
|
||||
verbose_name=_('bridge interface')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@@ -559,23 +581,25 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
|
||||
related_name='member_interfaces',
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name='Parent LAG'
|
||||
verbose_name=_('parent LAG')
|
||||
)
|
||||
type = models.CharField(
|
||||
verbose_name=_('type'),
|
||||
max_length=50,
|
||||
choices=InterfaceTypeChoices
|
||||
)
|
||||
mgmt_only = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name='Management only',
|
||||
verbose_name=_('management only'),
|
||||
help_text=_('This interface is used only for out-of-band management')
|
||||
)
|
||||
speed = models.PositiveIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name='Speed (Kbps)'
|
||||
verbose_name=_('speed (Kbps)')
|
||||
)
|
||||
duplex = models.CharField(
|
||||
verbose_name=_('duplex'),
|
||||
max_length=50,
|
||||
blank=True,
|
||||
null=True,
|
||||
@@ -584,27 +608,27 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
|
||||
wwn = WWNField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name='WWN',
|
||||
verbose_name=_('WWN'),
|
||||
help_text=_('64-bit World Wide Name')
|
||||
)
|
||||
rf_role = models.CharField(
|
||||
max_length=30,
|
||||
choices=WirelessRoleChoices,
|
||||
blank=True,
|
||||
verbose_name='Wireless role'
|
||||
verbose_name=_('wireless role')
|
||||
)
|
||||
rf_channel = models.CharField(
|
||||
max_length=50,
|
||||
choices=WirelessChannelChoices,
|
||||
blank=True,
|
||||
verbose_name='Wireless channel'
|
||||
verbose_name=_('wireless channel')
|
||||
)
|
||||
rf_channel_frequency = models.DecimalField(
|
||||
max_digits=7,
|
||||
decimal_places=2,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name='Channel frequency (MHz)',
|
||||
verbose_name=_('channel frequency (MHz)'),
|
||||
help_text=_("Populated by selected channel (if set)")
|
||||
)
|
||||
rf_channel_width = models.DecimalField(
|
||||
@@ -612,26 +636,26 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
|
||||
decimal_places=3,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name='Channel width (MHz)',
|
||||
verbose_name=('channel width (MHz)'),
|
||||
help_text=_("Populated by selected channel (if set)")
|
||||
)
|
||||
tx_power = models.PositiveSmallIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=(MaxValueValidator(127),),
|
||||
verbose_name='Transmit power (dBm)'
|
||||
verbose_name=_('transmit power (dBm)')
|
||||
)
|
||||
poe_mode = models.CharField(
|
||||
max_length=50,
|
||||
choices=InterfacePoEModeChoices,
|
||||
blank=True,
|
||||
verbose_name='PoE mode'
|
||||
verbose_name=_('PoE mode')
|
||||
)
|
||||
poe_type = models.CharField(
|
||||
max_length=50,
|
||||
choices=InterfacePoETypeChoices,
|
||||
blank=True,
|
||||
verbose_name='PoE type'
|
||||
verbose_name=_('PoE type')
|
||||
)
|
||||
wireless_link = models.ForeignKey(
|
||||
to='wireless.WirelessLink',
|
||||
@@ -644,7 +668,7 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
|
||||
to='wireless.WirelessLAN',
|
||||
related_name='interfaces',
|
||||
blank=True,
|
||||
verbose_name='Wireless LANs'
|
||||
verbose_name=_('wireless LANs')
|
||||
)
|
||||
untagged_vlan = models.ForeignKey(
|
||||
to='ipam.VLAN',
|
||||
@@ -652,13 +676,13 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
|
||||
related_name='interfaces_as_untagged',
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name='Untagged VLAN'
|
||||
verbose_name=_('untagged VLAN')
|
||||
)
|
||||
tagged_vlans = models.ManyToManyField(
|
||||
to='ipam.VLAN',
|
||||
related_name='interfaces_as_tagged',
|
||||
blank=True,
|
||||
verbose_name='Tagged VLANs'
|
||||
verbose_name=_('tagged VLANs')
|
||||
)
|
||||
vrf = models.ForeignKey(
|
||||
to='ipam.VRF',
|
||||
@@ -666,7 +690,7 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
|
||||
related_name='interfaces',
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name='VRF'
|
||||
verbose_name=_('VRF')
|
||||
)
|
||||
ip_addresses = GenericRelation(
|
||||
to='ipam.IPAddress',
|
||||
@@ -704,77 +728,98 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
|
||||
# Virtual Interfaces cannot have a Cable attached
|
||||
if self.is_virtual and self.cable:
|
||||
raise ValidationError({
|
||||
'type': f"{self.get_type_display()} interfaces cannot have a cable attached."
|
||||
'type': _("{display_type} interfaces cannot have a cable attached.").format(
|
||||
display_type=self.get_type_display()
|
||||
)
|
||||
})
|
||||
|
||||
# Virtual Interfaces cannot be marked as connected
|
||||
if self.is_virtual and self.mark_connected:
|
||||
raise ValidationError({
|
||||
'mark_connected': f"{self.get_type_display()} interfaces cannot be marked as connected."
|
||||
'mark_connected': _("{display_type} interfaces cannot be marked as connected.".format(
|
||||
display_type=self.get_type_display())
|
||||
)
|
||||
})
|
||||
|
||||
# Parent validation
|
||||
|
||||
# An interface cannot be its own parent
|
||||
if self.pk and self.parent_id == self.pk:
|
||||
raise ValidationError({'parent': "An interface cannot be its own parent."})
|
||||
raise ValidationError({'parent': _("An interface cannot be its own parent.")})
|
||||
|
||||
# A physical interface cannot have a parent interface
|
||||
if self.type != InterfaceTypeChoices.TYPE_VIRTUAL and self.parent is not None:
|
||||
raise ValidationError({'parent': "Only virtual interfaces may be assigned to a parent interface."})
|
||||
raise ValidationError({'parent': _("Only virtual interfaces may be assigned to a parent interface.")})
|
||||
|
||||
# An interface's parent must belong to the same device or virtual chassis
|
||||
if self.parent and self.parent.device != self.device:
|
||||
if self.device.virtual_chassis is None:
|
||||
raise ValidationError({
|
||||
'parent': f"The selected parent interface ({self.parent}) belongs to a different device "
|
||||
f"({self.parent.device})."
|
||||
'parent': _(
|
||||
"The selected parent interface ({interface}) belongs to a different device ({device})"
|
||||
).format(interface=self.parent, device=self.parent.device)
|
||||
})
|
||||
elif self.parent.device.virtual_chassis != self.parent.virtual_chassis:
|
||||
raise ValidationError({
|
||||
'parent': f"The selected parent interface ({self.parent}) belongs to {self.parent.device}, which "
|
||||
f"is not part of virtual chassis {self.device.virtual_chassis}."
|
||||
'parent': _(
|
||||
"The selected parent interface ({interface}) belongs to {device}, which is not part of "
|
||||
"virtual chassis {virtual_chassis}."
|
||||
).format(
|
||||
interface=self.parent,
|
||||
device=self.parent_device,
|
||||
virtual_chassis=self.device.virtual_chassis
|
||||
)
|
||||
})
|
||||
|
||||
# Bridge validation
|
||||
|
||||
# An interface cannot be bridged to itself
|
||||
if self.pk and self.bridge_id == self.pk:
|
||||
raise ValidationError({'bridge': "An interface cannot be bridged to itself."})
|
||||
raise ValidationError({'bridge': _("An interface cannot be bridged to itself.")})
|
||||
|
||||
# A bridged interface belong to the same device or virtual chassis
|
||||
if self.bridge and self.bridge.device != self.device:
|
||||
if self.device.virtual_chassis is None:
|
||||
raise ValidationError({
|
||||
'bridge': f"The selected bridge interface ({self.bridge}) belongs to a different device "
|
||||
f"({self.bridge.device})."
|
||||
'bridge': _("""
|
||||
The selected bridge interface ({bridge}) belongs to a different device
|
||||
({device}).""").format(bridge=self.bridge, device=self.bridge.device)
|
||||
})
|
||||
elif self.bridge.device.virtual_chassis != self.device.virtual_chassis:
|
||||
raise ValidationError({
|
||||
'bridge': f"The selected bridge interface ({self.bridge}) belongs to {self.bridge.device}, which "
|
||||
f"is not part of virtual chassis {self.device.virtual_chassis}."
|
||||
'bridge': _(
|
||||
"The selected bridge interface ({interface}) belongs to {device}, which is not part of virtual "
|
||||
"chassis {virtual_chassis}."
|
||||
).format(
|
||||
interface=self.bridge, device=self.bridge.device, virtual_chassis=self.device.virtual_chassis
|
||||
)
|
||||
})
|
||||
|
||||
# LAG validation
|
||||
|
||||
# A virtual interface cannot have a parent LAG
|
||||
if self.type == InterfaceTypeChoices.TYPE_VIRTUAL and self.lag is not None:
|
||||
raise ValidationError({'lag': "Virtual interfaces cannot have a parent LAG interface."})
|
||||
raise ValidationError({'lag': _("Virtual interfaces cannot have a parent LAG interface.")})
|
||||
|
||||
# A LAG interface cannot be its own parent
|
||||
if self.pk and self.lag_id == self.pk:
|
||||
raise ValidationError({'lag': "A LAG interface cannot be its own parent."})
|
||||
raise ValidationError({'lag': _("A LAG interface cannot be its own parent.")})
|
||||
|
||||
# An interface's LAG must belong to the same device or virtual chassis
|
||||
if self.lag and self.lag.device != self.device:
|
||||
if self.device.virtual_chassis is None:
|
||||
raise ValidationError({
|
||||
'lag': f"The selected LAG interface ({self.lag}) belongs to a different device ({self.lag.device})."
|
||||
'lag': _(
|
||||
"The selected LAG interface ({lag}) belongs to a different device ({device})."
|
||||
).format(lag=self.lag, device=self.lag.device)
|
||||
})
|
||||
elif self.lag.device.virtual_chassis != self.device.virtual_chassis:
|
||||
raise ValidationError({
|
||||
'lag': f"The selected LAG interface ({self.lag}) belongs to {self.lag.device}, which is not part "
|
||||
f"of virtual chassis {self.device.virtual_chassis}."
|
||||
'lag': _(
|
||||
"The selected LAG interface ({lag}) belongs to {device}, which is not part of virtual chassis "
|
||||
"{virtual_chassis}.".format(
|
||||
lag=self.lag, device=self.lag.device, virtual_chassis=self.device.virtual_chassis)
|
||||
)
|
||||
})
|
||||
|
||||
# PoE validation
|
||||
@@ -782,52 +827,54 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
|
||||
# Only physical interfaces may have a PoE mode/type assigned
|
||||
if self.poe_mode and self.is_virtual:
|
||||
raise ValidationError({
|
||||
'poe_mode': "Virtual interfaces cannot have a PoE mode."
|
||||
'poe_mode': _("Virtual interfaces cannot have a PoE mode.")
|
||||
})
|
||||
if self.poe_type and self.is_virtual:
|
||||
raise ValidationError({
|
||||
'poe_type': "Virtual interfaces cannot have a PoE type."
|
||||
'poe_type': _("Virtual interfaces cannot have a PoE type.")
|
||||
})
|
||||
|
||||
# An interface with a PoE type set must also specify a mode
|
||||
if self.poe_type and not self.poe_mode:
|
||||
raise ValidationError({
|
||||
'poe_type': "Must specify PoE mode when designating a PoE type."
|
||||
'poe_type': _("Must specify PoE mode when designating a PoE type.")
|
||||
})
|
||||
|
||||
# Wireless validation
|
||||
|
||||
# RF role & channel may only be set for wireless interfaces
|
||||
if self.rf_role and not self.is_wireless:
|
||||
raise ValidationError({'rf_role': "Wireless role may be set only on wireless interfaces."})
|
||||
raise ValidationError({'rf_role': _("Wireless role may be set only on wireless interfaces.")})
|
||||
if self.rf_channel and not self.is_wireless:
|
||||
raise ValidationError({'rf_channel': "Channel may be set only on wireless interfaces."})
|
||||
raise ValidationError({'rf_channel': _("Channel may be set only on wireless interfaces.")})
|
||||
|
||||
# Validate channel frequency against interface type and selected channel (if any)
|
||||
if self.rf_channel_frequency:
|
||||
if not self.is_wireless:
|
||||
raise ValidationError({
|
||||
'rf_channel_frequency': "Channel frequency may be set only on wireless interfaces.",
|
||||
'rf_channel_frequency': _("Channel frequency may be set only on wireless interfaces."),
|
||||
})
|
||||
if self.rf_channel and self.rf_channel_frequency != get_channel_attr(self.rf_channel, 'frequency'):
|
||||
raise ValidationError({
|
||||
'rf_channel_frequency': "Cannot specify custom frequency with channel selected.",
|
||||
'rf_channel_frequency': _("Cannot specify custom frequency with channel selected."),
|
||||
})
|
||||
|
||||
# Validate channel width against interface type and selected channel (if any)
|
||||
if self.rf_channel_width:
|
||||
if not self.is_wireless:
|
||||
raise ValidationError({'rf_channel_width': "Channel width may be set only on wireless interfaces."})
|
||||
raise ValidationError({'rf_channel_width': _("Channel width may be set only on wireless interfaces.")})
|
||||
if self.rf_channel and self.rf_channel_width != get_channel_attr(self.rf_channel, 'width'):
|
||||
raise ValidationError({'rf_channel_width': "Cannot specify custom width with channel selected."})
|
||||
raise ValidationError({'rf_channel_width': _("Cannot specify custom width with channel selected.")})
|
||||
|
||||
# VLAN validation
|
||||
|
||||
# Validate untagged VLAN
|
||||
if self.untagged_vlan and self.untagged_vlan.site not in [self.device.site, None]:
|
||||
raise ValidationError({
|
||||
'untagged_vlan': f"The untagged VLAN ({self.untagged_vlan}) must belong to the same site as the "
|
||||
f"interface's parent device, or it must be global."
|
||||
'untagged_vlan': _("""
|
||||
The untagged VLAN ({untagged_vlan}) must belong to the same site as the
|
||||
interface's parent device, or it must be global.
|
||||
""").format(untagged_vlan=self.untagged_vlan)
|
||||
})
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
@@ -894,10 +941,12 @@ class FrontPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
|
||||
A pass-through port on the front of a Device.
|
||||
"""
|
||||
type = models.CharField(
|
||||
verbose_name=_('type'),
|
||||
max_length=50,
|
||||
choices=PortTypeChoices
|
||||
)
|
||||
color = ColorField(
|
||||
verbose_name=_('color'),
|
||||
blank=True
|
||||
)
|
||||
rear_port = models.ForeignKey(
|
||||
@@ -906,6 +955,7 @@ class FrontPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
|
||||
related_name='frontports'
|
||||
)
|
||||
rear_port_position = models.PositiveSmallIntegerField(
|
||||
verbose_name=_('rear port position'),
|
||||
default=1,
|
||||
validators=[
|
||||
MinValueValidator(REARPORT_POSITIONS_MIN),
|
||||
@@ -939,14 +989,22 @@ class FrontPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
|
||||
# Validate rear port assignment
|
||||
if self.rear_port.device != self.device:
|
||||
raise ValidationError({
|
||||
"rear_port": f"Rear port ({self.rear_port}) must belong to the same device"
|
||||
"rear_port": _(
|
||||
"Rear port ({rear_port}) must belong to the same device"
|
||||
).format(rear_port=self.rear_port)
|
||||
})
|
||||
|
||||
# Validate rear port position assignment
|
||||
if self.rear_port_position > self.rear_port.positions:
|
||||
raise ValidationError({
|
||||
"rear_port_position": f"Invalid rear port position ({self.rear_port_position}): Rear port "
|
||||
f"{self.rear_port.name} has only {self.rear_port.positions} positions"
|
||||
"rear_port_position": _(
|
||||
"Invalid rear port position ({rear_port_position}): Rear port {name} has only {positions} "
|
||||
"positions."
|
||||
).format(
|
||||
rear_port_position=self.rear_port_position,
|
||||
name=self.rear_port.name,
|
||||
positions=self.rear_port.positions
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -955,13 +1013,16 @@ class RearPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
|
||||
A pass-through port on the rear of a Device.
|
||||
"""
|
||||
type = models.CharField(
|
||||
verbose_name=_('type'),
|
||||
max_length=50,
|
||||
choices=PortTypeChoices
|
||||
)
|
||||
color = ColorField(
|
||||
verbose_name=_('color'),
|
||||
blank=True
|
||||
)
|
||||
positions = models.PositiveSmallIntegerField(
|
||||
verbose_name=_('positions'),
|
||||
default=1,
|
||||
validators=[
|
||||
MinValueValidator(REARPORT_POSITIONS_MIN),
|
||||
@@ -982,8 +1043,9 @@ class RearPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
|
||||
frontport_count = self.frontports.count()
|
||||
if self.positions < frontport_count:
|
||||
raise ValidationError({
|
||||
"positions": f"The number of positions cannot be less than the number of mapped front ports "
|
||||
f"({frontport_count})"
|
||||
"positions": _("""
|
||||
The number of positions cannot be less than the number of mapped front ports
|
||||
({frontport_count})""").format(frontport_count=frontport_count)
|
||||
})
|
||||
|
||||
|
||||
@@ -996,6 +1058,7 @@ class ModuleBay(ComponentModel, TrackingModelMixin):
|
||||
An empty space within a Device which can house a child device
|
||||
"""
|
||||
position = models.CharField(
|
||||
verbose_name=_('position'),
|
||||
max_length=30,
|
||||
blank=True,
|
||||
help_text=_('Identifier to reference when renaming installed components')
|
||||
@@ -1014,7 +1077,7 @@ class DeviceBay(ComponentModel, TrackingModelMixin):
|
||||
installed_device = models.OneToOneField(
|
||||
to='dcim.Device',
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='parent_bay',
|
||||
related_name=_('parent_bay'),
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
@@ -1029,22 +1092,22 @@ class DeviceBay(ComponentModel, TrackingModelMixin):
|
||||
|
||||
# Validate that the parent Device can have DeviceBays
|
||||
if not self.device.device_type.is_parent_device:
|
||||
raise ValidationError("This type of device ({}) does not support device bays.".format(
|
||||
self.device.device_type
|
||||
raise ValidationError(_("This type of device ({device_type}) does not support device bays.").format(
|
||||
device_type=self.device.device_type
|
||||
))
|
||||
|
||||
# Cannot install a device into itself, obviously
|
||||
if self.device == self.installed_device:
|
||||
raise ValidationError("Cannot install a device into itself.")
|
||||
raise ValidationError(_("Cannot install a device into itself."))
|
||||
|
||||
# Check that the installed device is not already installed elsewhere
|
||||
if self.installed_device:
|
||||
current_bay = DeviceBay.objects.filter(installed_device=self.installed_device).first()
|
||||
if current_bay and current_bay != self:
|
||||
raise ValidationError({
|
||||
'installed_device': "Cannot install the specified device; device is already installed in {}".format(
|
||||
current_bay
|
||||
)
|
||||
'installed_device': _(
|
||||
"Cannot install the specified device; device is already installed in {bay}."
|
||||
).format(bay=current_bay)
|
||||
})
|
||||
|
||||
|
||||
@@ -1058,6 +1121,7 @@ class InventoryItemRole(OrganizationalModel):
|
||||
Inventory items may optionally be assigned a functional role.
|
||||
"""
|
||||
color = ColorField(
|
||||
verbose_name=_('color'),
|
||||
default=ColorChoices.COLOR_GREY
|
||||
)
|
||||
|
||||
@@ -1110,13 +1174,13 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin):
|
||||
)
|
||||
part_id = models.CharField(
|
||||
max_length=50,
|
||||
verbose_name='Part ID',
|
||||
verbose_name=_('part ID'),
|
||||
blank=True,
|
||||
help_text=_('Manufacturer-assigned part identifier')
|
||||
)
|
||||
serial = models.CharField(
|
||||
max_length=50,
|
||||
verbose_name='Serial number',
|
||||
verbose_name=_('serial number'),
|
||||
blank=True
|
||||
)
|
||||
asset_tag = models.CharField(
|
||||
@@ -1124,10 +1188,11 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin):
|
||||
unique=True,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name='Asset tag',
|
||||
verbose_name=_('asset tag'),
|
||||
help_text=_('A unique tag used to identify this item')
|
||||
)
|
||||
discovered = models.BooleanField(
|
||||
verbose_name=_('discovered'),
|
||||
default=False,
|
||||
help_text=_('This item was automatically discovered')
|
||||
)
|
||||
@@ -1154,7 +1219,7 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin):
|
||||
# An InventoryItem cannot be its own parent
|
||||
if self.pk and self.parent_id == self.pk:
|
||||
raise ValidationError({
|
||||
"parent": "Cannot assign self as parent."
|
||||
"parent": _("Cannot assign self as parent.")
|
||||
})
|
||||
|
||||
# Validation for moving InventoryItems
|
||||
@@ -1162,13 +1227,13 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin):
|
||||
# Cannot move an InventoryItem to another device if it has a parent
|
||||
if self.parent and self.parent.device != self.device:
|
||||
raise ValidationError({
|
||||
"parent": "Parent inventory item does not belong to the same device."
|
||||
"parent": _("Parent inventory item does not belong to the same device.")
|
||||
})
|
||||
|
||||
# Prevent moving InventoryItems with children
|
||||
first_child = self.get_children().first()
|
||||
if first_child and first_child.device != self.device:
|
||||
raise ValidationError("Cannot move an inventory item with dependent children")
|
||||
raise ValidationError(_("Cannot move an inventory item with dependent children"))
|
||||
|
||||
# When moving an InventoryItem to another device, remove any associated component
|
||||
if self.component and self.component.device != self.device:
|
||||
@@ -1176,5 +1241,5 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin):
|
||||
else:
|
||||
if self.component and self.component.device != self.device:
|
||||
raise ValidationError({
|
||||
"device": "Cannot assign inventory item to component on another device"
|
||||
"device": _("Cannot assign inventory item to component on another device")
|
||||
})
|
||||
|
||||
@@ -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
|
||||
@@ -176,7 +180,8 @@ class DeviceType(PrimaryModel, WeightMixin):
|
||||
)
|
||||
|
||||
clone_fields = (
|
||||
'manufacturer', 'default_platform', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit'
|
||||
'manufacturer', 'default_platform', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', 'weight',
|
||||
'weight_unit',
|
||||
)
|
||||
prerequisite_models = (
|
||||
'dcim.Manufacturer',
|
||||
@@ -277,7 +282,7 @@ class DeviceType(PrimaryModel, WeightMixin):
|
||||
# U height must be divisible by 0.5
|
||||
if decimal.Decimal(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 +298,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.
|
||||
@@ -306,23 +311,23 @@ class DeviceType(PrimaryModel, WeightMixin):
|
||||
if racked_instance_count:
|
||||
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 <a href="{url}">{racked_instance_count} instances</a> already '
|
||||
f'mounted within racks.'
|
||||
)
|
||||
'u_height': mark_safe(_(
|
||||
'Unable to set 0U height: Found <a href="{url}">{racked_instance_count} instances</a> already '
|
||||
'mounted within racks.'
|
||||
).format(url=url, racked_instance_count=racked_instance_count))
|
||||
})
|
||||
|
||||
if (
|
||||
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 +372,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 +461,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 +558,7 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
|
||||
null=True
|
||||
)
|
||||
name = models.CharField(
|
||||
verbose_name=_('name'),
|
||||
max_length=64,
|
||||
blank=True,
|
||||
null=True
|
||||
@@ -563,7 +572,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 +580,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 +608,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 +635,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 +643,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',
|
||||
@@ -640,7 +651,7 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
|
||||
related_name='+',
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name='Out-of-band IP'
|
||||
verbose_name=_('out-of-band IP')
|
||||
)
|
||||
cluster = models.ForeignKey(
|
||||
to='virtualization.Cluster',
|
||||
@@ -657,12 +668,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 +689,7 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
|
||||
null=True
|
||||
)
|
||||
latitude = models.DecimalField(
|
||||
verbose_name=_('latitude'),
|
||||
max_digits=8,
|
||||
decimal_places=6,
|
||||
blank=True,
|
||||
@@ -683,6 +697,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 +778,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 +814,48 @@ 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 +864,17 @@ 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 +885,12 @@ 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 +901,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 +909,14 @@ 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 +924,9 @@ 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 +942,25 @@ 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 +1145,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 +1153,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 +1183,9 @@ 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 +1283,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 +1315,9 @@ 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):
|
||||
@@ -1285,10 +1330,10 @@ class VirtualChassis(PrimaryModel):
|
||||
lag__device=F('device')
|
||||
)
|
||||
if interfaces:
|
||||
raise ProtectedError(
|
||||
f"Unable to delete virtual chassis {self}. There are member interfaces which form a cross-chassis LAG",
|
||||
interfaces
|
||||
)
|
||||
raise ProtectedError(_(
|
||||
"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 +1347,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 +1367,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 +1375,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 +1385,7 @@ class VirtualDeviceContext(PrimaryModel):
|
||||
null=True
|
||||
)
|
||||
comments = models.TextField(
|
||||
verbose_name=_('comments'),
|
||||
blank=True
|
||||
)
|
||||
|
||||
@@ -1382,7 +1431,9 @@ 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:
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -167,14 +178,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):
|
||||
|
||||
@@ -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]),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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
|
||||
@@ -178,9 +181,10 @@ class Site(PrimaryModel):
|
||||
null=True
|
||||
)
|
||||
facility = models.CharField(
|
||||
verbose_name=_('facility'),
|
||||
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 +195,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 +270,7 @@ class Location(NestedGroupModel):
|
||||
related_name='locations'
|
||||
)
|
||||
status = models.CharField(
|
||||
verbose_name=_('status'),
|
||||
max_length=50,
|
||||
choices=LocationStatusChoices,
|
||||
default=LocationStatusChoices.STATUS_ACTIVE
|
||||
@@ -304,7 +313,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 +323,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 +338,6 @@ 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))
|
||||
|
||||
Reference in New Issue
Block a user