diff --git a/netbox/templates/tenancy/contactassignment_edit.html b/netbox/templates/tenancy/contactassignment_edit.html
index 4d1747e72..d904deead 100644
--- a/netbox/templates/tenancy/contactassignment_edit.html
+++ b/netbox/templates/tenancy/contactassignment_edit.html
@@ -3,6 +3,9 @@
{% load form_helpers %}
{% block form %}
+ {% for field in form.hidden_fields %}
+ {{ field }}
+ {% endfor %}
Contact Assignment
diff --git a/netbox/tenancy/forms/models.py b/netbox/tenancy/forms/models.py
index 021e36a5b..eabcb1d0f 100644
--- a/netbox/tenancy/forms/models.py
+++ b/netbox/tenancy/forms/models.py
@@ -119,8 +119,10 @@ class ContactAssignmentForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = ContactAssignment
fields = (
- 'group', 'contact', 'role', 'priority',
+ 'content_type', 'object_id', 'group', 'contact', 'role', 'priority',
)
widgets = {
+ 'content_type': forms.HiddenInput(),
+ 'object_id': forms.HiddenInput(),
'priority': StaticSelect(),
}
diff --git a/netbox/tenancy/migrations/0001_squashed_0012.py b/netbox/tenancy/migrations/0001_squashed_0012.py
index 77297b982..e8a028a92 100644
--- a/netbox/tenancy/migrations/0001_squashed_0012.py
+++ b/netbox/tenancy/migrations/0001_squashed_0012.py
@@ -1,4 +1,4 @@
-import django.core.serializers.json
+from utilities.json import CustomFieldJSONEncoder
from django.db import migrations, models
import django.db.models.deletion
import mptt.fields
@@ -34,7 +34,7 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=100, unique=True)),
('slug', models.SlugField(max_length=100, unique=True)),
@@ -54,7 +54,7 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=100, unique=True)),
('slug', models.SlugField(max_length=100, unique=True)),
diff --git a/netbox/tenancy/migrations/0003_contacts.py b/netbox/tenancy/migrations/0003_contacts.py
index 35e568ab1..ba9bef50f 100644
--- a/netbox/tenancy/migrations/0003_contacts.py
+++ b/netbox/tenancy/migrations/0003_contacts.py
@@ -1,4 +1,4 @@
-import django.core.serializers.json
+from utilities.json import CustomFieldJSONEncoder
from django.db import migrations, models
import django.db.models.deletion
import mptt.fields
@@ -19,7 +19,7 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=100, unique=True)),
('slug', models.SlugField(max_length=100, unique=True)),
@@ -34,7 +34,7 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=100)),
('slug', models.SlugField(max_length=100)),
@@ -55,7 +55,7 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=100)),
('title', models.CharField(blank=True, max_length=100)),
diff --git a/netbox/utilities/filters.py b/netbox/utilities/filters.py
index 543449b73..d41eff498 100644
--- a/netbox/utilities/filters.py
+++ b/netbox/utilities/filters.py
@@ -3,8 +3,6 @@ from django import forms
from django.conf import settings
from django_filters.constants import EMPTY_VALUES
-from utilities.forms import MACAddressField
-
def multivalue_field_factory(field_class):
"""
@@ -23,7 +21,15 @@ def multivalue_field_factory(field_class):
field.to_python(v) for v in value if v
]
- return type('MultiValue{}'.format(field_class.__name__), (NewField,), dict())
+ def run_validators(self, value):
+ for v in value:
+ super().run_validators(v)
+
+ def validate(self, value):
+ for v in value:
+ super().validate(v)
+
+ return type(f'MultiValue{field_class.__name__}', (NewField,), dict())
#
@@ -46,6 +52,10 @@ class MultiValueNumberFilter(django_filters.MultipleChoiceFilter):
field_class = multivalue_field_factory(forms.IntegerField)
+class MultiValueDecimalFilter(django_filters.MultipleChoiceFilter):
+ field_class = multivalue_field_factory(forms.DecimalField)
+
+
class MultiValueTimeFilter(django_filters.MultipleChoiceFilter):
field_class = multivalue_field_factory(forms.TimeField)
diff --git a/netbox/utilities/json.py b/netbox/utilities/json.py
new file mode 100644
index 000000000..5574ff36f
--- /dev/null
+++ b/netbox/utilities/json.py
@@ -0,0 +1,17 @@
+import decimal
+
+from django.core.serializers.json import DjangoJSONEncoder
+
+__all__ = (
+ 'CustomFieldJSONEncoder',
+)
+
+
+class CustomFieldJSONEncoder(DjangoJSONEncoder):
+ """
+ Override Django's built-in JSON encoder to save decimal values as JSON numbers.
+ """
+ def default(self, o):
+ if isinstance(o, decimal.Decimal):
+ return float(o)
+ return super().default(o)
diff --git a/netbox/utilities/templatetags/helpers.py b/netbox/utilities/templatetags/helpers.py
index 67ed553b2..462b37feb 100644
--- a/netbox/utilities/templatetags/helpers.py
+++ b/netbox/utilities/templatetags/helpers.py
@@ -73,9 +73,9 @@ def humanize_megabytes(mb):
"""
if not mb:
return ''
- if mb >= 1048576:
+ if not mb % 1048576: # 1024^2
return f'{int(mb / 1048576)} TB'
- if mb >= 1024:
+ if not mb % 1024:
return f'{int(mb / 1024)} GB'
return f'{mb} MB'
diff --git a/netbox/utilities/utils.py b/netbox/utilities/utils.py
index 69ab615fc..9f587e88d 100644
--- a/netbox/utilities/utils.py
+++ b/netbox/utilities/utils.py
@@ -12,7 +12,7 @@ from django.http import QueryDict
from jinja2.sandbox import SandboxedEnvironment
from mptt.models import MPTTModel
-from dcim.choices import CableLengthUnitChoices
+from dcim.choices import CableLengthUnitChoices, WeightUnitChoices
from extras.plugins import PluginConfig
from extras.utils import is_taggable
from netbox.config import get_config
@@ -270,6 +270,31 @@ def to_meters(length, unit):
raise ValueError(f"Unknown unit {unit}. Must be 'km', 'm', 'cm', 'mi', 'ft', or 'in'.")
+def to_grams(weight, unit):
+ """
+ Convert the given weight to kilograms.
+ """
+ try:
+ if weight < 0:
+ raise ValueError("Weight must be a positive number")
+ except TypeError:
+ raise TypeError(f"Invalid value '{weight}' for weight (must be a number)")
+
+ valid_units = WeightUnitChoices.values()
+ if unit not in valid_units:
+ raise ValueError(f"Unknown unit {unit}. Must be one of the following: {', '.join(valid_units)}")
+
+ if unit == WeightUnitChoices.UNIT_KILOGRAM:
+ return weight * 1000
+ if unit == WeightUnitChoices.UNIT_GRAM:
+ return weight
+ if unit == WeightUnitChoices.UNIT_POUND:
+ return weight * Decimal(453.592)
+ if unit == WeightUnitChoices.UNIT_OUNCE:
+ return weight * Decimal(28.3495)
+ raise ValueError(f"Unknown unit {unit}. Must be 'kg', 'g', 'lb', 'oz'.")
+
+
def render_jinja2(template_code, context):
"""
Render a Jinja2 template with the provided context. Return the rendered content.
diff --git a/netbox/virtualization/migrations/0001_squashed_0022.py b/netbox/virtualization/migrations/0001_squashed_0022.py
index d00bae2e2..29eda8a50 100644
--- a/netbox/virtualization/migrations/0001_squashed_0022.py
+++ b/netbox/virtualization/migrations/0001_squashed_0022.py
@@ -1,5 +1,5 @@
import dcim.fields
-import django.core.serializers.json
+from utilities.json import CustomFieldJSONEncoder
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
@@ -51,7 +51,7 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=100, unique=True)),
('comments', models.TextField(blank=True)),
@@ -65,7 +65,7 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=100, unique=True)),
('slug', models.SlugField(max_length=100, unique=True)),
@@ -80,7 +80,7 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=100, unique=True)),
('slug', models.SlugField(max_length=100, unique=True)),
@@ -95,7 +95,7 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('local_context_data', models.JSONField(blank=True, null=True)),
('name', models.CharField(max_length=64)),
@@ -147,7 +147,7 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('enabled', models.BooleanField(default=True)),
('mac_address', dcim.fields.MACAddressField(blank=True, null=True)),
diff --git a/netbox/wireless/migrations/0001_wireless.py b/netbox/wireless/migrations/0001_wireless.py
index 10b6e585b..9369df8a5 100644
--- a/netbox/wireless/migrations/0001_wireless.py
+++ b/netbox/wireless/migrations/0001_wireless.py
@@ -1,4 +1,4 @@
-import django.core.serializers.json
+from utilities.json import CustomFieldJSONEncoder
from django.db import migrations, models
import django.db.models.deletion
import mptt.fields
@@ -21,7 +21,7 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=100, unique=True)),
('slug', models.SlugField(max_length=100, unique=True)),
@@ -44,7 +44,7 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('ssid', models.CharField(max_length=32)),
('group', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='wireless_lans', to='wireless.wirelesslangroup')),
@@ -65,7 +65,7 @@ class Migration(migrations.Migration):
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
- ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('ssid', models.CharField(blank=True, max_length=32)),
('status', models.CharField(default='connected', max_length=50)),