diff --git a/docs/release-notes/version-2.7.md b/docs/release-notes/version-2.7.md index 916c94cf5..cdc13953c 100644 --- a/docs/release-notes/version-2.7.md +++ b/docs/release-notes/version-2.7.md @@ -5,6 +5,7 @@ ## Enhancements * [#3949](https://github.com/netbox-community/netbox/issues/3949) - Revised the installation docs and upgrade script to employ a Python virtual environment +* [#4218](https://github.com/netbox-community/netbox/issues/4218) - Allow negative voltage for DC power feeds * [#4281](https://github.com/netbox-community/netbox/issues/4281) - Allow filtering device component list views by type * [#4284](https://github.com/netbox-community/netbox/issues/4284) - Add MRJ21 port and cable types * [#4290](https://github.com/netbox-community/netbox/issues/4290) - Include device name in tooltip on rack elevations diff --git a/netbox/dcim/migrations/0099_powerfeed_negative_voltage.py b/netbox/dcim/migrations/0099_powerfeed_negative_voltage.py new file mode 100644 index 000000000..db16fbc91 --- /dev/null +++ b/netbox/dcim/migrations/0099_powerfeed_negative_voltage.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.10 on 2020-03-03 16:59 + +from django.db import migrations, models +import utilities.validators + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0098_devicetype_images'), + ] + + operations = [ + migrations.AlterField( + model_name='powerfeed', + name='voltage', + field=models.SmallIntegerField(default=120, validators=[utilities.validators.ExclusionValidator([0])]), + ), + ] diff --git a/netbox/dcim/models/__init__.py b/netbox/dcim/models/__init__.py index 821b8eeb7..8b84c79d8 100644 --- a/netbox/dcim/models/__init__.py +++ b/netbox/dcim/models/__init__.py @@ -24,6 +24,7 @@ from extras.models import ConfigContextModel, CustomFieldModel, ObjectChange, Ta from utilities.fields import ColorField, NaturalOrderingField from utilities.models import ChangeLoggedModel from utilities.utils import serialize_object, to_meters +from utilities.validators import ExclusionValidator from .device_component_templates import ( ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, FrontPortTemplate, InterfaceTemplate, PowerOutletTemplate, PowerPortTemplate, RearPortTemplate, @@ -1775,9 +1776,9 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel): choices=PowerFeedPhaseChoices, default=PowerFeedPhaseChoices.PHASE_SINGLE ) - voltage = models.PositiveSmallIntegerField( - validators=[MinValueValidator(1)], - default=POWERFEED_VOLTAGE_DEFAULT + voltage = models.SmallIntegerField( + default=POWERFEED_VOLTAGE_DEFAULT, + validators=[ExclusionValidator([0])] ) amperage = models.PositiveSmallIntegerField( validators=[MinValueValidator(1)], @@ -1859,10 +1860,16 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel): 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" + }) + def save(self, *args, **kwargs): # Cache the available_power property on the instance - kva = self.voltage * self.amperage * (self.max_utilization / 100) + kva = abs(self.voltage) * self.amperage * (self.max_utilization / 100) if self.phase == PowerFeedPhaseChoices.PHASE_3PHASE: self.available_power = round(kva * 1.732) else: diff --git a/netbox/utilities/validators.py b/netbox/utilities/validators.py index cfa733208..3b08733cd 100644 --- a/netbox/utilities/validators.py +++ b/netbox/utilities/validators.py @@ -1,6 +1,6 @@ import re -from django.core.validators import _lazy_re_compile, URLValidator +from django.core.validators import _lazy_re_compile, BaseValidator, URLValidator class EnhancedURLValidator(URLValidator): @@ -26,3 +26,13 @@ class EnhancedURLValidator(URLValidator): r'(?:[/?#][^\s]*)?' # Path r'\Z', re.IGNORECASE) schemes = AnyURLScheme() + + +class ExclusionValidator(BaseValidator): + """ + Ensure that a field's value is not equal to any of the specified values. + """ + message = 'This value may not be %(show_value)s.' + + def compare(self, a, b): + return a in b