Arthur 5 дней назад
Родитель
Сommit
aab0f18323

+ 1 - 1
netbox/dcim/models/cooling.py

@@ -6,9 +6,9 @@ from django.utils.translation import gettext_lazy as _
 from dcim.choices import *
 from dcim.choices import *
 from netbox.models import PrimaryModel
 from netbox.models import PrimaryModel
 from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
 from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
-from netbox.models.mixins import CoolingTemperatureMixin, FlowRateMixin, PressureMixin
 
 
 from .device_components import CabledObjectModel, PathEndpoint
 from .device_components import CabledObjectModel, PathEndpoint
+from .mixins import CoolingTemperatureMixin, FlowRateMixin, PressureMixin
 
 
 __all__ = (
 __all__ = (
     'CoolingFeed',
     'CoolingFeed',

+ 1 - 2
netbox/dcim/models/device_component_templates.py

@@ -8,11 +8,10 @@ from django.utils.translation import gettext_lazy as _
 from dcim.choices import *
 from dcim.choices import *
 from dcim.constants import *
 from dcim.constants import *
 from dcim.models.base import PortMappingBase
 from dcim.models.base import PortMappingBase
-from dcim.models.mixins import InterfaceValidationMixin
+from dcim.models.mixins import DiameterMixin, InterfaceValidationMixin, MaximumFlowMixin
 from dcim.utils import get_module_bay_positions, resolve_module_placeholder
 from dcim.utils import get_module_bay_positions, resolve_module_placeholder
 from netbox.models import ChangeLoggedModel
 from netbox.models import ChangeLoggedModel
 from netbox.models.ltree import LtreeManager, LtreeModel
 from netbox.models.ltree import LtreeManager, LtreeModel
-from netbox.models.mixins import DiameterMixin, MaximumFlowMixin
 from utilities.fields import ColorField, NaturalOrderingField
 from utilities.fields import ColorField, NaturalOrderingField
 from utilities.ordering import naturalize_interface
 from utilities.ordering import naturalize_interface
 from utilities.tracking import TrackingModelMixin
 from utilities.tracking import TrackingModelMixin

+ 2 - 2
netbox/dcim/models/device_components.py

@@ -12,11 +12,11 @@ from dcim.choices import *
 from dcim.constants import *
 from dcim.constants import *
 from dcim.fields import WWNField
 from dcim.fields import WWNField
 from dcim.models.base import PortMappingBase
 from dcim.models.base import PortMappingBase
-from dcim.models.mixins import InterfaceValidationMixin
+from dcim.models.mixins import DiameterMixin, InterfaceValidationMixin, MaximumFlowMixin
 from netbox.choices import ColorChoices
 from netbox.choices import ColorChoices
 from netbox.models import NetBoxModel, OrganizationalModel
 from netbox.models import NetBoxModel, OrganizationalModel
 from netbox.models.ltree import LtreeManager, LtreeModel, SortPathField
 from netbox.models.ltree import LtreeManager, LtreeModel, SortPathField
-from netbox.models.mixins import DiameterMixin, MaximumFlowMixin, OwnerMixin
+from netbox.models.mixins import OwnerMixin
 from utilities.fields import ColorField, NaturalOrderingField
 from utilities.fields import ColorField, NaturalOrderingField
 from utilities.ordering import naturalize_interface
 from utilities.ordering import naturalize_interface
 from utilities.query_functions import CollateAsChar
 from utilities.query_functions import CollateAsChar

+ 307 - 0
netbox/dcim/models/mixins.py

@@ -1,14 +1,27 @@
 from django.apps import apps
 from django.apps import apps
 from django.contrib.contenttypes.fields import GenericForeignKey
 from django.contrib.contenttypes.fields import GenericForeignKey
 from django.core.exceptions import ValidationError
 from django.core.exceptions import ValidationError
+from django.core.validators import MinValueValidator
 from django.db import models
 from django.db import models
 from django.utils.translation import gettext_lazy as _
 from django.utils.translation import gettext_lazy as _
 
 
 from dcim.constants import VIRTUAL_IFACE_TYPES, WIRELESS_IFACE_TYPES
 from dcim.constants import VIRTUAL_IFACE_TYPES, WIRELESS_IFACE_TYPES
+from netbox.choices import *
+from utilities.conversion import (
+    to_celsius,
+    to_kilopascals,
+    to_liters_per_minute,
+    to_millimeters,
+)
 
 
 __all__ = (
 __all__ = (
     'CachedScopeMixin',
     'CachedScopeMixin',
+    'CoolingTemperatureMixin',
+    'DiameterMixin',
+    'FlowRateMixin',
     'InterfaceValidationMixin',
     'InterfaceValidationMixin',
+    'MaximumFlowMixin',
+    'PressureMixin',
     'RenderConfigMixin',
     'RenderConfigMixin',
 )
 )
 
 
@@ -150,3 +163,297 @@ class InterfaceValidationMixin:
         # RF role may be set only for wireless interfaces
         # RF role may be set only for wireless interfaces
         if self.rf_role and self.type not in WIRELESS_IFACE_TYPES:
         if self.rf_role and self.type not in WIRELESS_IFACE_TYPES:
             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.")})
+
+
+class DiameterMixin(models.Model):
+    diameter = models.DecimalField(
+        verbose_name=_('diameter'),
+        max_digits=8,
+        decimal_places=2,
+        blank=True,
+        null=True,
+    )
+    diameter_unit = models.CharField(
+        verbose_name=_('diameter unit'),
+        max_length=50,
+        choices=DiameterUnitChoices,
+        blank=True,
+        null=True,
+    )
+    # Stores the normalized diameter (in millimeters) for database ordering
+    _abs_diameter = models.DecimalField(
+        max_digits=13,
+        decimal_places=4,
+        blank=True,
+        null=True
+    )
+
+    class Meta:
+        abstract = True
+
+    @property
+    def abs_diameter(self):
+        # Public alias for _abs_diameter; Django templates cannot access underscore-prefixed attributes.
+        return self._abs_diameter
+
+    def save(self, *args, **kwargs):
+        # Store the given diameter (if any) in millimeters for use in database ordering
+        if self.diameter is not None and self.diameter_unit:
+            self._abs_diameter = to_millimeters(self.diameter, self.diameter_unit)
+        else:
+            self._abs_diameter = None
+
+        # Clear diameter_unit if no diameter is defined
+        if self.diameter is None:
+            self.diameter_unit = None
+
+        super().save(*args, **kwargs)
+
+    def clean(self):
+        super().clean()
+
+        # Validate diameter and diameter_unit
+        if self.diameter is not None and not self.diameter_unit:
+            raise ValidationError(_("Must specify a unit when setting a diameter"))
+
+
+class FlowRateMixin(models.Model):
+    flow_rate = models.DecimalField(
+        verbose_name=_('flow rate'),
+        max_digits=8,
+        decimal_places=2,
+        blank=True,
+        null=True,
+        validators=[MinValueValidator(0)],
+    )
+    flow_rate_unit = models.CharField(
+        verbose_name=_('flow rate unit'),
+        max_length=50,
+        choices=FlowRateUnitChoices,
+        blank=True,
+        null=True,
+    )
+    # Stores the normalized flow rate (in liters per minute) for database ordering
+    _abs_flow_rate = models.DecimalField(
+        max_digits=13,
+        decimal_places=4,
+        blank=True,
+        null=True
+    )
+
+    class Meta:
+        abstract = True
+
+    @property
+    def abs_flow_rate(self):
+        # Public alias for _abs_flow_rate; Django templates cannot access underscore-prefixed attributes.
+        return self._abs_flow_rate
+
+    def save(self, *args, **kwargs):
+        # Store the given flow rate (if any) in liters per minute for use in database ordering
+        if self.flow_rate is not None and self.flow_rate_unit:
+            self._abs_flow_rate = to_liters_per_minute(self.flow_rate, self.flow_rate_unit)
+        else:
+            self._abs_flow_rate = None
+
+        # Clear flow_rate_unit if no flow rate is defined
+        if self.flow_rate is None:
+            self.flow_rate_unit = None
+
+        super().save(*args, **kwargs)
+
+    def clean(self):
+        super().clean()
+
+        # Validate flow_rate and flow_rate_unit
+        if self.flow_rate is not None and not self.flow_rate_unit:
+            raise ValidationError(_("Must specify a unit when setting a flow rate"))
+
+
+class MaximumFlowMixin(models.Model):
+    maximum_flow = models.DecimalField(
+        verbose_name=_('maximum flow'),
+        max_digits=8,
+        decimal_places=2,
+        blank=True,
+        null=True,
+        validators=[MinValueValidator(0)],
+    )
+    maximum_flow_unit = models.CharField(
+        verbose_name=_('maximum flow unit'),
+        max_length=50,
+        choices=FlowRateUnitChoices,
+        blank=True,
+        null=True,
+    )
+    # Stores the normalized maximum flow (in liters per minute) for database ordering
+    _abs_maximum_flow = models.DecimalField(
+        max_digits=13,
+        decimal_places=4,
+        blank=True,
+        null=True
+    )
+
+    class Meta:
+        abstract = True
+
+    @property
+    def abs_maximum_flow(self):
+        # Public alias for _abs_maximum_flow; Django templates cannot access underscore-prefixed attributes.
+        return self._abs_maximum_flow
+
+    def save(self, *args, **kwargs):
+        # Store the given maximum flow (if any) in liters per minute for use in database ordering
+        if self.maximum_flow is not None and self.maximum_flow_unit:
+            self._abs_maximum_flow = to_liters_per_minute(self.maximum_flow, self.maximum_flow_unit)
+        else:
+            self._abs_maximum_flow = None
+
+        # Clear maximum_flow_unit if no maximum flow is defined
+        if self.maximum_flow is None:
+            self.maximum_flow_unit = None
+
+        super().save(*args, **kwargs)
+
+    def clean(self):
+        super().clean()
+
+        # Validate maximum_flow and maximum_flow_unit
+        if self.maximum_flow is not None and not self.maximum_flow_unit:
+            raise ValidationError(_("Must specify a unit when setting a maximum flow"))
+
+
+class PressureMixin(models.Model):
+    pressure = models.DecimalField(
+        verbose_name=_('pressure'),
+        max_digits=8,
+        decimal_places=2,
+        blank=True,
+        null=True,
+        validators=[MinValueValidator(0)],
+    )
+    pressure_unit = models.CharField(
+        verbose_name=_('pressure unit'),
+        max_length=50,
+        choices=PressureUnitChoices,
+        blank=True,
+        null=True,
+    )
+    # Stores the normalized pressure (in kilopascals) for database ordering
+    _abs_pressure = models.DecimalField(
+        max_digits=13,
+        decimal_places=4,
+        blank=True,
+        null=True
+    )
+
+    class Meta:
+        abstract = True
+
+    @property
+    def abs_pressure(self):
+        # Public alias for _abs_pressure; Django templates cannot access underscore-prefixed attributes.
+        return self._abs_pressure
+
+    def save(self, *args, **kwargs):
+        # Store the given pressure (if any) in kilopascals for use in database ordering
+        if self.pressure is not None and self.pressure_unit:
+            self._abs_pressure = to_kilopascals(self.pressure, self.pressure_unit)
+        else:
+            self._abs_pressure = None
+
+        # Clear pressure_unit if no pressure is defined
+        if self.pressure is None:
+            self.pressure_unit = None
+
+        super().save(*args, **kwargs)
+
+    def clean(self):
+        super().clean()
+
+        # Validate pressure and pressure_unit
+        if self.pressure is not None and not self.pressure_unit:
+            raise ValidationError(_("Must specify a unit when setting a pressure"))
+
+
+class CoolingTemperatureMixin(models.Model):
+    """
+    Adds supply/return temperatures sharing a single unit, with normalized (°C) fields for ordering.
+    """
+    supply_temperature = models.DecimalField(
+        verbose_name=_('supply temperature'),
+        max_digits=6,
+        decimal_places=2,
+        blank=True,
+        null=True,
+        help_text=_('Supply (cold) temperature')
+    )
+    return_temperature = models.DecimalField(
+        verbose_name=_('return temperature'),
+        max_digits=6,
+        decimal_places=2,
+        blank=True,
+        null=True,
+        help_text=_('Return (warm) temperature')
+    )
+    temperature_unit = models.CharField(
+        verbose_name=_('temperature unit'),
+        max_length=50,
+        choices=TemperatureUnitChoices,
+        blank=True,
+        null=True,
+    )
+    # Stores the normalized temperatures (in degrees Celsius) for database ordering
+    _abs_supply_temperature = models.DecimalField(
+        max_digits=8,
+        decimal_places=4,
+        blank=True,
+        null=True
+    )
+    _abs_return_temperature = models.DecimalField(
+        max_digits=8,
+        decimal_places=4,
+        blank=True,
+        null=True
+    )
+
+    class Meta:
+        abstract = True
+
+    @property
+    def abs_supply_temperature(self):
+        # Public alias for _abs_supply_temperature; Django templates cannot access underscore-prefixed attributes.
+        return self._abs_supply_temperature
+
+    @property
+    def abs_return_temperature(self):
+        # Public alias for _abs_return_temperature; Django templates cannot access underscore-prefixed attributes.
+        return self._abs_return_temperature
+
+    def save(self, *args, **kwargs):
+        # Store the given temperatures (if any) in degrees Celsius for use in database ordering
+        if self.temperature_unit:
+            self._abs_supply_temperature = (
+                to_celsius(self.supply_temperature, self.temperature_unit)
+                if self.supply_temperature is not None else None
+            )
+            self._abs_return_temperature = (
+                to_celsius(self.return_temperature, self.temperature_unit)
+                if self.return_temperature is not None else None
+            )
+        else:
+            self._abs_supply_temperature = None
+            self._abs_return_temperature = None
+
+        # Clear temperature_unit if no temperatures are defined
+        if self.supply_temperature is None and self.return_temperature is None:
+            self.temperature_unit = None
+
+        super().save(*args, **kwargs)
+
+    def clean(self):
+        super().clean()
+
+        # A temperature unit is required when a temperature is set
+        if (self.supply_temperature is not None or self.return_temperature is not None) and not self.temperature_unit:
+            raise ValidationError(_("Must specify a unit when setting a temperature"))

+ 1 - 308
netbox/netbox/models/mixins.py

@@ -1,26 +1,13 @@
 from django.core.exceptions import ValidationError
 from django.core.exceptions import ValidationError
-from django.core.validators import MinValueValidator
 from django.db import models
 from django.db import models
 from django.utils.translation import gettext_lazy as _
 from django.utils.translation import gettext_lazy as _
 
 
 from netbox.choices import *
 from netbox.choices import *
-from utilities.conversion import (
-    to_celsius,
-    to_grams,
-    to_kilopascals,
-    to_liters_per_minute,
-    to_meters,
-    to_millimeters,
-)
+from utilities.conversion import to_grams, to_meters
 
 
 __all__ = (
 __all__ = (
-    'CoolingTemperatureMixin',
-    'DiameterMixin',
     'DistanceMixin',
     'DistanceMixin',
-    'FlowRateMixin',
-    'MaximumFlowMixin',
     'OwnerMixin',
     'OwnerMixin',
-    'PressureMixin',
     'WeightMixin',
     'WeightMixin',
 )
 )
 
 
@@ -88,300 +75,6 @@ class WeightMixin(models.Model):
             raise ValidationError(_("Must specify a unit when setting a weight"))
             raise ValidationError(_("Must specify a unit when setting a weight"))
 
 
 
 
-class DiameterMixin(models.Model):
-    diameter = models.DecimalField(
-        verbose_name=_('diameter'),
-        max_digits=8,
-        decimal_places=2,
-        blank=True,
-        null=True,
-    )
-    diameter_unit = models.CharField(
-        verbose_name=_('diameter unit'),
-        max_length=50,
-        choices=DiameterUnitChoices,
-        blank=True,
-        null=True,
-    )
-    # Stores the normalized diameter (in millimeters) for database ordering
-    _abs_diameter = models.DecimalField(
-        max_digits=13,
-        decimal_places=4,
-        blank=True,
-        null=True
-    )
-
-    class Meta:
-        abstract = True
-
-    @property
-    def abs_diameter(self):
-        # Public alias for _abs_diameter; Django templates cannot access underscore-prefixed attributes.
-        return self._abs_diameter
-
-    def save(self, *args, **kwargs):
-        # Store the given diameter (if any) in millimeters for use in database ordering
-        if self.diameter is not None and self.diameter_unit:
-            self._abs_diameter = to_millimeters(self.diameter, self.diameter_unit)
-        else:
-            self._abs_diameter = None
-
-        # Clear diameter_unit if no diameter is defined
-        if self.diameter is None:
-            self.diameter_unit = None
-
-        super().save(*args, **kwargs)
-
-    def clean(self):
-        super().clean()
-
-        # Validate diameter and diameter_unit
-        if self.diameter is not None and not self.diameter_unit:
-            raise ValidationError(_("Must specify a unit when setting a diameter"))
-
-
-class FlowRateMixin(models.Model):
-    flow_rate = models.DecimalField(
-        verbose_name=_('flow rate'),
-        max_digits=8,
-        decimal_places=2,
-        blank=True,
-        null=True,
-        validators=[MinValueValidator(0)],
-    )
-    flow_rate_unit = models.CharField(
-        verbose_name=_('flow rate unit'),
-        max_length=50,
-        choices=FlowRateUnitChoices,
-        blank=True,
-        null=True,
-    )
-    # Stores the normalized flow rate (in liters per minute) for database ordering
-    _abs_flow_rate = models.DecimalField(
-        max_digits=13,
-        decimal_places=4,
-        blank=True,
-        null=True
-    )
-
-    class Meta:
-        abstract = True
-
-    @property
-    def abs_flow_rate(self):
-        # Public alias for _abs_flow_rate; Django templates cannot access underscore-prefixed attributes.
-        return self._abs_flow_rate
-
-    def save(self, *args, **kwargs):
-        # Store the given flow rate (if any) in liters per minute for use in database ordering
-        if self.flow_rate is not None and self.flow_rate_unit:
-            self._abs_flow_rate = to_liters_per_minute(self.flow_rate, self.flow_rate_unit)
-        else:
-            self._abs_flow_rate = None
-
-        # Clear flow_rate_unit if no flow rate is defined
-        if self.flow_rate is None:
-            self.flow_rate_unit = None
-
-        super().save(*args, **kwargs)
-
-    def clean(self):
-        super().clean()
-
-        # Validate flow_rate and flow_rate_unit
-        if self.flow_rate is not None and not self.flow_rate_unit:
-            raise ValidationError(_("Must specify a unit when setting a flow rate"))
-
-
-class MaximumFlowMixin(models.Model):
-    maximum_flow = models.DecimalField(
-        verbose_name=_('maximum flow'),
-        max_digits=8,
-        decimal_places=2,
-        blank=True,
-        null=True,
-        validators=[MinValueValidator(0)],
-    )
-    maximum_flow_unit = models.CharField(
-        verbose_name=_('maximum flow unit'),
-        max_length=50,
-        choices=FlowRateUnitChoices,
-        blank=True,
-        null=True,
-    )
-    # Stores the normalized maximum flow (in liters per minute) for database ordering
-    _abs_maximum_flow = models.DecimalField(
-        max_digits=13,
-        decimal_places=4,
-        blank=True,
-        null=True
-    )
-
-    class Meta:
-        abstract = True
-
-    @property
-    def abs_maximum_flow(self):
-        # Public alias for _abs_maximum_flow; Django templates cannot access underscore-prefixed attributes.
-        return self._abs_maximum_flow
-
-    def save(self, *args, **kwargs):
-        # Store the given maximum flow (if any) in liters per minute for use in database ordering
-        if self.maximum_flow is not None and self.maximum_flow_unit:
-            self._abs_maximum_flow = to_liters_per_minute(self.maximum_flow, self.maximum_flow_unit)
-        else:
-            self._abs_maximum_flow = None
-
-        # Clear maximum_flow_unit if no maximum flow is defined
-        if self.maximum_flow is None:
-            self.maximum_flow_unit = None
-
-        super().save(*args, **kwargs)
-
-    def clean(self):
-        super().clean()
-
-        # Validate maximum_flow and maximum_flow_unit
-        if self.maximum_flow is not None and not self.maximum_flow_unit:
-            raise ValidationError(_("Must specify a unit when setting a maximum flow"))
-
-
-class PressureMixin(models.Model):
-    pressure = models.DecimalField(
-        verbose_name=_('pressure'),
-        max_digits=8,
-        decimal_places=2,
-        blank=True,
-        null=True,
-        validators=[MinValueValidator(0)],
-    )
-    pressure_unit = models.CharField(
-        verbose_name=_('pressure unit'),
-        max_length=50,
-        choices=PressureUnitChoices,
-        blank=True,
-        null=True,
-    )
-    # Stores the normalized pressure (in kilopascals) for database ordering
-    _abs_pressure = models.DecimalField(
-        max_digits=13,
-        decimal_places=4,
-        blank=True,
-        null=True
-    )
-
-    class Meta:
-        abstract = True
-
-    @property
-    def abs_pressure(self):
-        # Public alias for _abs_pressure; Django templates cannot access underscore-prefixed attributes.
-        return self._abs_pressure
-
-    def save(self, *args, **kwargs):
-        # Store the given pressure (if any) in kilopascals for use in database ordering
-        if self.pressure is not None and self.pressure_unit:
-            self._abs_pressure = to_kilopascals(self.pressure, self.pressure_unit)
-        else:
-            self._abs_pressure = None
-
-        # Clear pressure_unit if no pressure is defined
-        if self.pressure is None:
-            self.pressure_unit = None
-
-        super().save(*args, **kwargs)
-
-    def clean(self):
-        super().clean()
-
-        # Validate pressure and pressure_unit
-        if self.pressure is not None and not self.pressure_unit:
-            raise ValidationError(_("Must specify a unit when setting a pressure"))
-
-
-class CoolingTemperatureMixin(models.Model):
-    """
-    Adds supply/return temperatures sharing a single unit, with normalized (°C) fields for ordering.
-    """
-    supply_temperature = models.DecimalField(
-        verbose_name=_('supply temperature'),
-        max_digits=6,
-        decimal_places=2,
-        blank=True,
-        null=True,
-        help_text=_('Supply (cold) temperature')
-    )
-    return_temperature = models.DecimalField(
-        verbose_name=_('return temperature'),
-        max_digits=6,
-        decimal_places=2,
-        blank=True,
-        null=True,
-        help_text=_('Return (warm) temperature')
-    )
-    temperature_unit = models.CharField(
-        verbose_name=_('temperature unit'),
-        max_length=50,
-        choices=TemperatureUnitChoices,
-        blank=True,
-        null=True,
-    )
-    # Stores the normalized temperatures (in degrees Celsius) for database ordering
-    _abs_supply_temperature = models.DecimalField(
-        max_digits=8,
-        decimal_places=4,
-        blank=True,
-        null=True
-    )
-    _abs_return_temperature = models.DecimalField(
-        max_digits=8,
-        decimal_places=4,
-        blank=True,
-        null=True
-    )
-
-    class Meta:
-        abstract = True
-
-    @property
-    def abs_supply_temperature(self):
-        # Public alias for _abs_supply_temperature; Django templates cannot access underscore-prefixed attributes.
-        return self._abs_supply_temperature
-
-    @property
-    def abs_return_temperature(self):
-        # Public alias for _abs_return_temperature; Django templates cannot access underscore-prefixed attributes.
-        return self._abs_return_temperature
-
-    def save(self, *args, **kwargs):
-        # Store the given temperatures (if any) in degrees Celsius for use in database ordering
-        if self.temperature_unit:
-            self._abs_supply_temperature = (
-                to_celsius(self.supply_temperature, self.temperature_unit)
-                if self.supply_temperature is not None else None
-            )
-            self._abs_return_temperature = (
-                to_celsius(self.return_temperature, self.temperature_unit)
-                if self.return_temperature is not None else None
-            )
-        else:
-            self._abs_supply_temperature = None
-            self._abs_return_temperature = None
-
-        # Clear temperature_unit if no temperatures are defined
-        if self.supply_temperature is None and self.return_temperature is None:
-            self.temperature_unit = None
-
-        super().save(*args, **kwargs)
-
-    def clean(self):
-        super().clean()
-
-        # A temperature unit is required when a temperature is set
-        if (self.supply_temperature is not None or self.return_temperature is not None) and not self.temperature_unit:
-            raise ValidationError(_("Must specify a unit when setting a temperature"))
-
-
 class DistanceMixin(models.Model):
 class DistanceMixin(models.Model):
     distance = models.DecimalField(
     distance = models.DecimalField(
         verbose_name=_('distance'),
         verbose_name=_('distance'),